NDB 쿼리

애플리케이션은 쿼리를 사용하여 필터라고 하는 특정한 검색 기준과 일치하는 Datastore 항목을 검색할 수 있습니다.

개요

애플리케이션은 쿼리를 사용하여 필터라고 하는 특정한 검색 기준과 일치하는 Datastore 항목을 검색할 수 있습니다. 예를 들어 여러 방명록을 추적하는 애플리케이션은 쿼리를 사용하여 방명록 한 개에서 메시지를 검색하고 이를 날짜순으로 정렬할 수 있습니다.

from google.appengine.ext import ndb
...
class Greeting(ndb.Model):
    """Models an individual Guestbook entry with content and date."""
    content = ndb.StringProperty()
    date = ndb.DateTimeProperty(auto_now_add=True)

    @classmethod
    def query_book(cls, ancestor_key):
        return cls.query(ancestor=ancestor_key).order(-cls.date)
...
class MainPage(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 20

    def get(self):
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key('Book', guestbook_name or '*notitle*')
        greetings = Greeting.query_book(ancestor_key).fetch(
            self.GREETINGS_PER_PAGE)

        self.response.out.write('<html><body>')

        for greeting in greetings:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        self.response.out.write('</body></html>')

일부 쿼리는 다른 쿼리보다 복잡하고, 이 경우 데이터 저장소에 사전 구성된 색인이 필요합니다. 이러한 사전 빌드된 색인은 구성 파일 index.yaml에 지정됩니다. 개발 서버에서 지정하지 않은 색인이 필요한 쿼리를 실행하면 개발 서버가 자동으로 index.yaml에 추가합니다. 하지만 웹 사이트에서 아직 지정되지 않은 색인이 필요한 쿼리는 실패합니다. 따라서 일반적으로 개발 서버에서 새 쿼리를 시도한 후 자동으로 변경된 index.yaml을 사용하도록 웹사이트를 업데이트하는 순으로 개발이 진행됩니다. gcloud app deploy index.yaml을 실행하여 애플리케이션 업로드와 별도로 index.yaml을 업데이트할 수 있습니다. Datastore에 항목이 많으면 새로운 색인을 만드는 데 시간이 오래 걸립니다. 이 경우 새 인덱스를 사용하는 코드를 업로드하기 전에 색인 정의를 업데이트하는 것이 좋습니다. 관리 콘솔을 사용하여 색인 빌드가 끝나는 시기를 알 수 있습니다.

App Engine Datastore는 일치검색(== 연산자)과 비교(<, <=, >, >= 연산자) 필터를 기본으로 지원합니다. 약간의 제약이 따르지만 부울 AND 연산을 사용하여 여러 필터를 결합할 수 있습니다(아래 참조).

기본 연산자 외에도 API는 != 연산자, 부울 OR 연산을 사용한 필터 그룹 결합, 가능한 값 목록 중 하나에 대한 같음을 테스트하는 IN 연산(예: Python의 'in' 연산자)을 지원합니다. 이러한 연산은 Datastore의 기본 연산과 1:1로 매핑되지 않아 약간 까다롭고 상대적으로 느리고, 결과 스트림의 메모리 내 병합을 사용하여 구현됩니다. p != v는 'p < v OR p > v'로 구현됩니다. (반복 속성에 중요합니다.)

제한: Datastore가 쿼리에 일부 제약을 가합니다. 이러한 제약을 위반하면 예외가 발생합니다. 예를 들어 너무 많은 필터 결합, 다수의 속성에 대해 부등식 사용 또는 부등식을 다른 속성의 정렬 순서와 결합은 현재 허용되지 않습니다. 또한, 여러 속성을 참조하는 필터에는 보조 색인을 구성해야 하는 경우가 있습니다.

지원되지 않음: Datastore는 하위 문자열 일치, 대소문자를 구분하지 않는 일치 또는 이른바 전체 텍스트 검색을 직접 지원하지 않습니다. 계산된 속성을 사용하여 대소 문자를 구분하지 않는 일치와 심지어 전체 텍스트 검색을 구현하는 방법이 있습니다.

속성 값 필터링

NDB 속성에서 Account 클래스를 호출합니다.

class Account(ndb.Model):
    username = ndb.StringProperty()
    userid = ndb.IntegerProperty()
    email = ndb.StringProperty()

일반적으로 지정된 종류의 모든 항목을 검색하지 않고 일부 속성에 대한 특정 값 또는 값의 범위를 가진 항목만 검색하려 합니다.

속성 객체는 일부 연산자를 오버로드하여 쿼리를 제어하는 데 사용될 수 있는 필터식을 반환합니다. 예를 들어 다음 식을 사용하여 userid 속성이 정확히 42 값을 갖는 모든 Account 항목을 찾을 수 있습니다.

query = Account.query(Account.userid == 42)

(해당 useridAccount가 하나만 있는 것이 확실하면 userid를 키로 사용하는 것이 좋습니다. Account.get_by_id(...)Account.query(...).get()보다 빠릅니다.)

NDB는 다음 연산을 지원합니다.

property == value
property < value
property <= value
property > value
property >= value
property != value
property.IN([value1, value2])

다음과 같은 구문을 사용하여 부등식을 필터링할 수 있습니다.

query = Account.query(Account.userid >= 40)

이 구문은 userid 속성이 40 이상인 모든 Account 항목을 찾습니다.

이러한 연산 중 두 가지인 != 및 IN은 다른 요소와 조합하여 구현되며 != 및 IN에서의 설명과 같이 약간 까다롭습니다.

필터 여러 개를 지정할 수 있습니다.

query = Account.query(Account.userid >= 40, Account.userid < 50)

이 구문은 지정된 필터 인수를 결합하여 userid 값이 40 이상이고 50 미만인 모든 Account 항목을 반환합니다.

참고: 앞서 언급한 바와 같이 Datastore는 속성 한 개 이상에서 부등식 필터링을 사용하는 쿼리를 거부합니다.

단일 표현식에서 전체 쿼리 필터를 지정하는 대신 여러 단계로 빌드하는 것이 더 편리할 수 있습니다. 예를 들면 다음과 같습니다.

query1 = Account.query()  # Retrieve all Account entitites
query2 = query1.filter(Account.userid >= 40)  # Filter on userid >= 40
query3 = query2.filter(Account.userid < 50)  # Filter on userid < 50 too

query3는 이전 예시의 query 변수와 같습니다. 쿼리 객체는 변경될 수 없으므로 query2 구성은 query1에 영향을 미치지 않고 query3 구성은 query1 또는 query2에 영향을 미치지 않습니다.

!= 및 IN 연산

NDB 속성에서 Article 클래스를 호출합니다.

class Article(ndb.Model):
    title = ndb.StringProperty()
    stars = ndb.IntegerProperty()
    tags = ndb.StringProperty(repeated=True)

!=(같지 않음)와 IN(멤버십) 연산은 OR 연산을 사용하여 다른 필터를 결합하는 방식으로 구현됩니다. 이 중에서 첫 번째는 다음과 같습니다.

property != value

이는 아래와 같이 구현됩니다.

(property < value) OR (property > value)

예:

query = Article.query(Article.tags != 'perl')

는 다음과 동일합니다.

query = Article.query(ndb.OR(Article.tags < 'perl',
                             Article.tags > 'perl'))

참고: 의외로 이 쿼리는 'perl'을 태그로 포함하지 않는 Article 항목을 검색하지 않습니다. 오히려 태그가 최소 한 개 이상이고 'perl'이 아닌 모든 항목을 찾습니다. 예를 들어 'perl'이 태그 중 하나로 포함되어 있지만 결과에는 다음 항목이 포함됩니다.

Article(title='Perl + Python = Parrot',
        stars=5,
        tags=['python', 'perl'])

하지만 다음은 포함되지 않습니다.

Article(title='Introduction to Perl',
        stars=3,
        tags=['perl'])

'perl'과 동일한 태그를 포함하지 않는 항목을 쿼리할 수 없습니다.

마찬가지로 가능한 값 목록에서 멤버십을 테스트하는 IN 연산은 다음과 같습니다.

property IN [value1, value2, ...]

이는 다음과 같이 구현됩니다.

(property == value1) OR (property == value2) OR ...

예:

query = Article.query(Article.tags.IN(['python', 'ruby', 'php']))

는 다음과 동일합니다.

query = Article.query(ndb.OR(Article.tags == 'python',
                             Article.tags == 'ruby',
                             Article.tags == 'php'))

참고: OR을 사용하여 쿼리하면 중복 결과가 제거됩니다. 즉, 항목이 2개 이상의 서브 쿼리와 일치하더라도 결과 스트림에는 항목이 한 번 이상 포함되지 않습니다.

반복되는 속성 쿼리

이전 섹션에서 정의된 Article 클래스는 반복되는 속성 쿼리의 예시이기도 합니다. 특히, 다음과 같은 필터

Article.tags == 'python'

하지만 Article.tags는 반복되는 속성입니다. 반복되는 속성을 목록 객체와 비교할 수 없으며(Datastore가 인식하지 못함),

Article.tags.IN(['python', 'ruby', 'php'])

이 필터는 해당 태그 값이 ['python', 'ruby', 'php'] 목록인 Article 항목을 검색하는 것과는 완전히 다른 작업을 수행합니다. tags 값(목록으로 취급됨)에 해당 값 중 최소한 한 개 이상 포함된 항목을 검색합니다.

반복 속성의 None 값 쿼리에는 정의되지 않은 동작이 있습니다. 이 동작을 실행하지 마십시오.

AND 및 OR 연산 결합

ANDOR 연산을 임의로 중첩할 수 있습니다. 예를 들면 다음과 같습니다.

query = Article.query(ndb.AND(Article.tags == 'python',
                              ndb.OR(Article.tags.IN(['ruby', 'jruby']),
                                     ndb.AND(Article.tags == 'php',
                                             Article.tags != 'perl'))))

하지만 OR 구현으로 인해 이 형식의 쿼리가 너무 복잡하면 예외가 발생하여 실패할 수 있습니다. 표현식 트리 맨 위에 단일 OR 연산이 (최대) 한 개 있고, 그 아래에 단일 수준의 AND 연산이 있도록 해당 필터를 정규화하면 더욱 안전합니다.

이 정규화를 수행하려면 부울 논리 규칙과 !=IN 필터가 실제로 구현되는 방식을 모두 기억해야 합니다.

  1. !=IN 연산자를 기본 형태로 확장합니다. 여기서 !=는 속성이 값보다 크거나 작은지 확인하고, IN은 속성이 첫 번째 값 또는 두 번째 값과 같은지 확인합니다. 이런 방식으로 목록의 마지막 값까지 진행됩니다.
  2. OR가 내부에 있는 AND는 원래 AND 피연산자에 적용된 여러 ANDOR와 동일하고, 원래 OR에 대해 단일 OR 피연산자가 대체됩니다. 예를 들어 AND(a, b, OR(c, d))OR(AND(a, b, c), AND(a, b, d))와 동일합니다.
  3. 그 자체가 AND 연산인 피연산자가 있는 AND은 중첩된 AND의 피연산자를 바깥쪽 AND에 도입할 수 있습니다. 예를 들어 AND(a, b, AND(c, d))AND(a, b, c, d)와 동일합니다.
  4. 그 자체가 OR 연산인 피연산자가 있는 OR은 중첩된 OR의 피연산자를 바깥쪽 OR에 도입할 수 있습니다. 예를 들어 OR(a, b, OR(c, d))OR(a, b, c, d)와 동일합니다.

Python보다 단순한 표기법을 사용하여 예시 필터에 이러한 변환을 단계별로 적용하면 다음을 얻을 수 있습니다.

  1. IN!= 연산자에서 규칙 #1 사용:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php',
             OR(tags < 'perl', tags > 'perl'))))
  2. AND 내에 중첩된 가장 안쪽의 OR에서 규칙 #2 사용:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         OR(AND(tags == 'php', tags < 'perl'),
            AND(tags == 'php', tags > 'perl'))))
  3. 다른 OR 내에 중첩된 OR에서 규칙 #4 사용:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php', tags < 'perl'),
         AND(tags == 'php', tags > 'perl')))
  4. AND 내에 중첩된 나머지 OR에서 규칙 #2 사용:
    OR(AND(tags == 'python', tags == 'ruby'),
       AND(tags == 'python', tags == 'jruby'),
       AND(tags == 'python', AND(tags == 'php', tags < 'perl')),
       AND(tags == 'python', AND(tags == 'php', tags > 'perl')))
  5. 규칙 #3을 사용하여 남은 중첩 AND 확장:
    OR(AND(tags == 'python', tags == 'ruby'),
       AND(tags == 'python', tags == 'jruby'),
       AND(tags == 'python', tags == 'php', tags < 'perl'),
       AND(tags == 'python', tags == 'php', tags > 'perl'))

주의: 일부 필터의 경우, 이 정규화로 인해 조합적 폭발이 발생할 수 있습니다. 기본 절이 각각 2개 있는 OR 절 3개의 AND를 고려하세요. 정규화되면 기본 절이 각각 3개 있는 AND 절 8개의 OR이 됩니다. 즉, 항 6개가 24개로 됩니다.

정렬 순서 지정

order() 메서드를 사용하여 쿼리가 결과를 반환하는 순서를 지정할 수 있습니다. 이 메서드는 인수 목록을 취하며, 이 목록의 각 인수는 속성 객체(오름차순으로 정렬됨) 또는 그 부정(내림차순을 표시함)입니다. 예를 들면 다음과 같습니다.

query = Greeting.query().order(Greeting.content, -Greeting.date)

모든 Greeting 항목이 검색되고 해당 content 속성의 오름차순 값을 기준으로 정렬됩니다. 동일한 콘텐츠 속성을 사용하여 연속 항목을 실행하면 해당 date 속성의 내림차순 값을 기준으로 정렬됩니다. 동일한 효과를 얻기 위해 여러 order() 호출을 사용할 수 있습니다.

query = Greeting.query().order(Greeting.content).order(-Greeting.date)

참고: order()로 필터를 결합하면 Datastore는 특정 조합을 거부합니다. 특히 불일치 필터를 사용하는 경우, 첫 번째 정렬 순서(있는 경우)는 필터와 같은 속성을 지정해야 합니다. 또한 두 번째 색인을 구성해야 할 때도 있습니다.

상위 쿼리

상위 쿼리를 사용하면 Datastore에 strong consistency를 갖는 쿼리를 할 수 있지만 동일한 상위를 가진 항목은 초당 1회 쓰기로 제한됩니다. Datastore의 고객과 관련 구매를 사용하여 상위 쿼리와 비 상위 쿼리 간의 장단점 및 구조를 간단히 비교하겠습니다.

상위가 아닌 다음 예시에서는 각 Customer의 Datastore에 하나의 항목, 각 Purchase의 Datastore에 하나의 항목이 고객을 가리키는 KeyProperty와 함께 있습니다.

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    customer = ndb.KeyProperty(kind=Customer)
    price = ndb.IntegerProperty()

고객에게 속한 모든 구매를 찾으려면 다음 쿼리를 사용할 수 있습니다.

purchases = Purchase.query(
    Purchase.customer == customer_entity.key).fetch()

이 경우 Datastore는 높은 쓰기 처리량을 제공하지만 eventual consistency만 제공합니다. 새 구매가 추가되면 부실 데이터가 발생할 수 있습니다. 상위 쿼리를 사용하여 이 동작을 제거할 수 있습니다.

상위 쿼리를 사용하는 고객과 구매의 경우, 개별 항목 두 개를 포함한 동일한 구조가 계속 유지됩니다. 고객 부분은 동일합니다. 하지만 구매를 만들 때 더 이상 KeyProperty()를 구매에 지정할 필요가 없습니다. 상위 쿼리를 사용하는 경우, 구매 항목을 생성하면 고객 항목의 키를 호출하기 때문입니다.

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    price = ndb.IntegerProperty()

각 구매에는 키가 있고 고객도 고유한 키를 갖습니다. 그러나 각 구매 키에는 customer_entity의 키가 포함됩니다. 상위 쿼리는 1초에 쓰기 1회로 제한된다는 점을 기억하세요. 다음은 상위가 있는 항목을 생성합니다.

purchase = Purchase(parent=customer_entity.key)

특정 고객의 구매를 쿼리하려면 다음 쿼리를 사용합니다.

purchases = Purchase.query(ancestor=customer_entity.key).fetch()

쿼리 속성

쿼리 객체에는 다음과 같은 읽기 전용 데이터 속성이 있습니다.

속성유형기본값설명
종류str None 종류 이름(일반적으로 클래스 이름)
상위Key None 쿼리에 지정된 상위
필터FilterNode None 필터 표현식
순서Order None 정렬 순서

쿼리 객체를 인쇄하면(또는 이 객체에서 str() 또는 repr() 호출) 잘 정돈된 문자열 표현이 생성됩니다.

print(Employee.query())
# -> Query(kind='Employee')
print(Employee.query(ancestor=ndb.Key(Manager, 1)))
# -> Query(kind='Employee', ancestor=Key('Manager', 1))

구조화된 속성 값 필터링

쿼리는 구조화된 속성의 필드 값을 직접 필터링할 수 있습니다. 예를 들어 도시가 'Amsterdam'인 주소를 가진 모든 연락처에 대한 쿼리는 다음과 같습니다.

query = Contact.query(Contact.addresses.city == 'Amsterdam')

이러한 필터를 여러 개 결합하면 필터가 동일한 연락처 항목 내에서 다른 Address 하위 항목을 일치시킬 수 있습니다. 예를 들면 다음과 같습니다.

query = Contact.query(Contact.addresses.city == 'Amsterdam',  # Beware!
                      Contact.addresses.street == 'Spear St')

도시가 'Amsterdam'인 주소와 거리가 'Spear St'인 또 다른(상이한) 주소를 가진 연락처를 찾을 수 있습니다. 그러나 적어도 등식 필터의 경우 단일 하위 항목에 여러 값이 있는 결과만 반환하는 쿼리를 만들 수 있습니다.

query = Contact.query(Contact.addresses == Address(city='San Francisco',
                                                   street='Spear St'))

이 기법을 사용하면 None과 동일한 하위 항목의 속성이 쿼리에서 무시됩니다. 속성에 기본값이 있으면 명시적으로 None으로 설정하여 쿼리에서 무시해야 합니다. 그렇지 않으면 쿼리에 속성 값이 기본 값과 같아야 하는 필터가 포함됩니다. 예를 들어 Address 모델에 default='us'인 속성 country가 있는 경우, 위 예시에서는 국가가 'us'인 연락처만 반환합니다. 다른 국가 값을 가진 연락처를 고려하려면 Address(city='San Francisco', street='Spear St', country=None)을 필터링해야 합니다.

하위 항목에 None인 속성 값이 있으면 무시됩니다. 따라서 하위 항목 속성 값인 None을 필터링하는 것은 의미가 없습니다.

문자열로 명명된 속성 사용

때때로 문자열로 이름이 지정된 속성을 기반으로 쿼리를 필터링하거나 정렬하려 할 때가 있습니다. 예를 들어 사용자에게 tags:python과 같은 검색 쿼리를 입력하도록 하면 어떻게든 이를 편리하게 다음과 같은 쿼리로 변환할 수 있습니다.

Article.query(Article."tags" == "python") # does NOT work

모델이 Expando이면 필터는 GenericProperty를 사용할 수 있고 Expando 클래스는 동적 속성을 사용합니다.

property_to_query = 'location'
query = FlexEmployee.query(ndb.GenericProperty(property_to_query) == 'SF')

모델이 Expando가 아닌 경우에도 GenericProperty를 사용할 수 있지만 정의된 속성 이름만 사용하려면 _properties 클래스 속성을 사용할 수도 있습니다.

query = Article.query(Article._properties[keyword] == value)

getattr()를 사용하여 클래스에서 이를 가져올 수도 있습니다.

query = Article.query(getattr(Article, keyword) == value)

getattr()은 속성의 'Python 이름'을 사용하고 _properties는 속성의 'Datastore 이름'을 기준으로 색인을 생성한다는 점이 다릅니다. 속성이 다음과 같이 선언된 경우에만 다릅니다.

class ArticleWithDifferentDatastoreName(ndb.Model):
    title = ndb.StringProperty('t')

여기서 Python 이름은 title이지만 Datastore 이름은 t입니다.

쿼리 결과를 정렬하는 데도 이러한 방법을 사용할 수 있습니다.

expando_query = FlexEmployee.query().order(ndb.GenericProperty('location'))

property_query = Article.query().order(Article._properties[keyword])

쿼리 반복기

쿼리가 진행되는 동안 상태는 반복기 객체에서 보관됩니다. (대부분의 애플리케이션은 이를 직접 사용하지 않습니다. 일반적으로 iterator 객체를 조작하는 것보다 fetch(20)를 호출하는 것이 더 간단합니다.) 이러한 객체를 가져오는 기본 방법은 두 개 있습니다.

  • Query 객체에서 Python의 기본 제공되는 iter() 함수 사용
  • Query 객체의 iter() 메서드 호출

첫 번째 방법은 쿼리를 반복 실행하기 위해 Python for 루프(iter() 함수를 암시적으로 호출) 사용을 지원합니다.

for greeting in greetings:
    self.response.out.write(
        '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

두 번째 방법은 Query 객체의 iter() 메서드를 통해 iterator에 옵션을 전달하여 동작에 영향을 주는 것입니다. 예를 들어 for 루프에서 키 전용 쿼리를 사용하려면 다음과 같이 작성합니다.

for key in query.iter(keys_only=True):
    print(key)

쿼리 반복기에는 다른 유용한 메서드가 있습니다.

메서드설명
__iter__() Python 반복기 프로토콜의 일부입니다.
next() 다음 결과를 반환하거나 결과가 없으면 StopIteration 예외를 발생시킵니다.

has_next() 이후 next() 호출이 결과를 반환하면 True를 반환하고 StopIteration을 발생시키면 False를 반환합니다.

이 질문에 대한 대답이 알려질 때까지 차단하고 next()를 통해 검색할 때까지 결과(있는 경우)를 버퍼링합니다.
probably_has_next() has_next()와 비슷하지만 보다 빠른(때로는 부정확한) 단축키를 사용합니다.

거짓양성(next()가 실제로 StopIteration을 발생시키는 경우의 True)을 반환할 수 있지만 거짓음성(next()가 실제로 결과를 반환하는 경우의 False)을 절대로 반환할 수 없습니다.
cursor_before() 마지막 결과가 반환되기 바로 전에 점을 나타내는 쿼리 커서를 반환합니다.

커서를 사용할 수 없는 경우에 예외가 발생합니다(특히 produce_cursors 쿼리 옵션이 전달되지 않은 경우).
cursor_after() 마지막 결과가 반환된 바로 후에 점을 나타내는 쿼리 커서를 반환합니다.

커서를 사용할 수 없는 경우에 예외가 발생합니다(특히 produce_cursors 쿼리 옵션이 전달되지 않은 경우).
index_list() 기본, 복합, 종류, 단일 속성 색인을 비롯하여 실행된 쿼리에서 사용되는 색인 목록을 반환합니다.

쿼리 커서

쿼리 커서는 쿼리의 다시 시작 지점을 나타내는 작고 불투명한 데이터 구조로, 사용자에게 한 번에 결과 한 페이지를 보여주는 데 유용합니다. 중지하고 다시 시작해야 할 수 있는 긴 작업 처리 시에도 유용합니다. 이를 위해 일반적으로 쿼리의 fetch_page() 메서드가 사용됩니다. fetch()와 다소 비슷하게 작동하지만 삼중 (results, cursor, more)를 반환합니다. more 플래그가 반환되면 더 많은 결과가 있을 수 있음을 나타냅니다. 예를 들어 UI는 이를 사용하여 '다음 페이지' 버튼 또는 링크를 숨길 수 있습니다. 이후 페이지를 요청하려면 fetch_page() 호출 한 개로 반환된 커서를 다음 호출로 전달합니다. 잘못된 커서에서 전달하면 BadArgumentError가 발생합니다. 유효성 검사는 값이 base64로 인코딩되었는지 여부만 확인합니다. 추가로 필요한 유효성 검사를 직접 수행해야 합니다.

따라서 한 번에 한 페이지씩 가져와서 사용자가 쿼리와 일치하는 모든 항목을 볼 수 있게 하려면 다음과 같은 코드가 필요합니다.

from google.appengine.datastore.datastore_query import Cursor
...
class List(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 10

    def get(self):
        """Handles requests like /list?cursor=1234567."""
        cursor = Cursor(urlsafe=self.request.get('cursor'))
        greets, next_cursor, more = Greeting.query().fetch_page(
            self.GREETINGS_PER_PAGE, start_cursor=cursor)

        self.response.out.write('<html><body>')

        for greeting in greets:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        if more and next_cursor:
            self.response.out.write('<a href="/list?cursor=%s">More...</a>' %
                                    next_cursor.urlsafe())

        self.response.out.write('</body></html>')

urlsafe()Cursor(urlsafe=s)를 사용하여 커서를 직렬화 및 역직렬화할 수 있습니다. 이를 통해 하나의 요청에 대한 응답으로 웹에서 클라이언트에 커서를 전달하고 이후 요청에서 클라이언트로부터 커서를 돌려 받을 수 있습니다.

참고: fetch_page() 메서드는 일반적으로 더 이상 결과가 없는 경우에도 커서를 반환하지만 이를 보장하지 않습니다. 즉, 반환되는 커서 값이 None일 수도 있습니다. 또한 more 플래그는 iterator의 probably_has_next() 메서드를 사용하여 구현되므로, 드물게 다음 페이지가 비어 있어도 True가 반환될 수 있습니다.

일부 NDB 쿼리는 쿼리 커서를 지원하지 않지만 이를 수정할 수 있습니다. 쿼리가 IN, OR 또는 !=를 사용하는 경우, 키에 의해 순서가 지정되지 않는 한 쿼리 결과는 커서를 통해 작동하지 않습니다. 애플리케이션에서 키를 기준으로 결과를 정렬하지 않고 fetch_page()를 호출하면 BadArgumentError가 반환됩니다. User.query(User.name.IN(['Joe', 'Jane'])).order(User.name).fetch_page(N) 가 오류를 수신하면 User.query(User.name.IN(['Joe', 'Jane'])).order(User.name, User.key).fetch_page(N)로 변경합니다.

쿼리 결과를 '페이징'하는 대신 쿼리의 iter() 메서드를 사용하여 정확한 지점에 커서를 가져올 수 있습니다. 이렇게 하려면 produce_cursors=Trueiter()에 전달합니다. iterator가 올바른 위치에 있으면 cursor_after()를 호출하여 바로 뒤에 있는 커서를 가져옵니다. (또는 바로 앞의 커서를 가져오려면 이와 비슷하게 cursor_before()를 호출합니다.) cursor_after() 또는 cursor_before()를 호출하면 차단 Datastore를 호출하고 배치 중간을 가리키는 커서를 가져오기 위해 쿼리의 일부를 다시 실행할 수 있습니다.

커서를 사용하여 쿼리 결과 페이지를 역방향으로 페이징하려면 역쿼리를 만듭니다.

# Set up.
q = Bar.query()
q_forward = q.order(Bar.key)
q_reverse = q.order(-Bar.key)

# Fetch a page going forward.
bars, cursor, more = q_forward.fetch_page(10)

# Fetch the same page going backward.
r_bars, r_cursor, r_more = q_reverse.fetch_page(10, start_cursor=cursor)

각 항목에 대한 함수 호출('매핑')

쿼리에서 반환된 Message 항목에 해당하는 Account 항목을 가져와야 한다고 가정합니다. 다음과 같이 작성할 수 있습니다.

message_account_pairs = []
for message in message_query:
    key = ndb.Key('Account', message.userid)
    account = key.get()
    message_account_pairs.append((message, account))

하지만 이는 매우 비효율적입니다. 항목을 가져오기 위해 기다린 후 이 항목을 사용하고 다시 다음 항목을 기다렸다가 사용하기 때문입니다. 대기하는 시간이 많습니다. 또 다른 방법은 쿼리 결과에 매핑되는 콜백 함수를 작성하는 것입니다.

def callback(message):
    key = ndb.Key('Account', message.userid)
    account = key.get()
    return message, account

message_account_pairs = message_query.map(callback)
# Now message_account_pairs is a list of (message, account) tuples.

일부 동시 실행이 가능하므로, 이 버전은 위의 단순한 for 루프보다 다소 빠르게 실행됩니다. 하지만 callback()get() 호출도 여전히 동기식이므로, 이점은 크지 않습니다. 비동기 gets을 사용하는 것이 좋습니다.

GQL

GQL은 App Engine Datastore에서 항목이나 키를 검색하기 위한 SQL과 유사한 언어입니다. GQL 기능은 기존 관계형 데이터베이스를 위한 쿼리 언어와 다르지만 GQL 구문은 SQL 구문과 비슷합니다. GQL 구문은 GQL 참조에 설명되어 있습니다.

GQL을 사용하여 쿼리를 구성할 수 있습니다. 이는 Model.query()를 사용하여 쿼리를 만드는 것과 유사하지만 GQL 구문을 사용하여 쿼리 필터와 순서를 정의합니다. 사용하려면 다음 안내를 따르세요.

  • ndb.gql(querystring)Query 객체(Model.query()에서 반환하는 것과 동일한 유형)를 반환합니다. 이러한 Query 객체(예: fetch(), map_async(), filter() 등)에서 모든 일반적인 메서드를 사용할 수 있습니다.
  • Model.gql(querystring)ndb.gql("SELECT * FROM Model " + querystring)의 줄임말입니다. 일반적으로 querystring"WHERE prop1 > 0 AND prop2 = TRUE"와 같습니다.
  • 구조화된 속성을 포함한 모델을 쿼리하려면 GQL 구문에서 foo.bar를 사용하여 하위 속성을 참조할 수 있습니다.
  • GQL은 SQL과 유사한 매개변수 바인딩을 지원합니다. 애플리케이션은 쿼리를 정의한 후 여기에 값을 결합할 수 있습니다.
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1")
    query2 = query.bind(3)
    
    또는
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1", 3)

    쿼리의 bind() 함수를 호출하면 새 쿼리가 반환됩니다. 원본은 변경되지 않습니다.

  • 모델 클래스가 _get_kind() 클래스 메서드를 재정의하는 경우 GQL 쿼리는 클래스 이름이 아니라 해당 함수에 의해 반환된 종류를 사용해야 합니다.
  • 모델의 속성이 이름을 재정의하는 경우(예: foo = StringProperty('bar')) GQL 쿼리는 재정의된 속성 이름(예: bar)을 사용해야 합니다.

쿼리의 일부 값이 사용자가 제공한 변수이면 항상 매개변수 바인딩 기능을 사용합니다. 이렇게 하면 구문 해킹에 기반한 공격을 피할 수 있습니다.

가져오지 않은(더 일반적으로 말하면, 정의되지 않은) 모델을 쿼리하면 오류가 발생합니다.

해당 모델이 Expando가 아닌 경우, 모델 클래스에 의해 정의되지 않은 속성 이름을 사용하면 오류가 발생합니다.

쿼리의 fetch()에 제한이나 오프셋을 지정하면 GQL의 OFFSETLIMIT 절에 의해 설정된 제한이나 오프셋이 재정의됩니다. GQL의 OFFSETLIMITfetch_page()와 결합하지 마세요. 쿼리에서 App Engine이 부과하는 최대 1,000개 결과는 offset과 limit 모두에 적용됩니다.

SQL에 익숙하다면 GQL을 사용하면서 잘못된 가정을 하지 마세요. GQL은 NDB의 기본 쿼리 API로 변환됩니다. 이는 데이터베이스 서버로 전송되기 전에 API 호출이 SQL로 변환되는 일반적인 객체 관계형 매퍼(SQLAlchemy 또는 Django의 데이터베이스 지원 등)와 다릅니다. GQL은 Datastore의 수정(삽입, 삭제 또는 업데이트)을 지원하지 않으며 쿼리만 지원합니다.