שאילתות 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)

יש שאילתות מורכבות יותר מאחרות, ומאגר הנתונים צריך אינדקסים מוכנים מראש בשבילן. האינדקסים המוגדרים מראש האלה מצוינים בקובץ תצורה, index.yaml. בשרת הפיתוח, אם מריצים שאילתה שצריכה אינדקס שלא צוין, שרת הפיתוח מוסיף אותו אוטומטית ל-index.yaml. אבל באתר שלכם, שאילתה שצריכה אינדקס שעדיין לא צוין נכשלת. לכן, מחזור הפיתוח הטיפוסי הוא לנסות שאילתה חדשה בשרת הפיתוח ואז לעדכן את האתר כך שישתמש ב-index.yaml שהשתנה אוטומטית. אפשר לעדכן את index.yaml בנפרד מהעלאת האפליקציה על ידי הרצת gcloud app deploy index.yaml. אם במאגר הנתונים יש הרבה ישויות, ייקח הרבה זמן ליצור עבורן אינדקס חדש. במקרה כזה, מומלץ לעדכן את הגדרות האינדקס לפני שמעלים קוד שמשתמש באינדקס החדש. אפשר להשתמש במסוף Admin כדי לדעת מתי בניית האינדקסים הסתיימה.

ה-App Engine Datastore תומך באופן מובנה במסננים של התאמות מדויקות (האופרטור ==) והשוואות (האופרטורים <, <=, > ו->=). הוא תומך בשילוב של כמה מסננים באמצעות פעולת AND בוליאנית, עם כמה מגבלות (מפורטות בהמשך).

בנוסף לאופרטורים המקוריים, ה-API תומך באופרטור !=, שמשלב קבוצות של מסננים באמצעות הפעולה הבוליאנית OR, ובפעולה IN, שבודקת שוויון לאחד מהערכים האפשריים ברשימה (כמו האופרטור in של Python). הפעולות האלה לא ממופות 1:1 לפעולות המקוריות של Datastore, ולכן הן קצת מוזרות ואיטיות יחסית. הן מיושמות באמצעות מיזוג בזיכרון של זרמי תוצאות. שימו לב שהפעולה p != v מיושמת כ-p < v OR p > v. (זה חשוב למאפיינים חוזרים).

מגבלות: Datastore אוכף כמה הגבלות על שאילתות. הפרה של ההגבלות האלה תגרום להעלאת חריגים. לדוגמה, אי אפשר לשלב יותר מדי מסננים, להשתמש באי-שוויון בכמה מאפיינים או לשלב אי-שוויון עם סדר מיון במאפיין אחר. בנוסף, לפעמים צריך להגדיר אינדקסים משניים כדי לסנן הפניות לכמה מאפיינים.

לא נתמך: Datastore לא תומך ישירות בהתאמות של מחרוזות משנה, בהתאמות שלא תלויות באותיות רישיות או קטנות, או במה שנקרא חיפוש טקסט מלא. יש דרכים להטמיע התאמות לא תלויות-רישיות ואפילו חיפוש טקסט מלא באמצעות מאפיינים מחושבים.

סינון לפי ערכי מאפיינים

נזכרים במחלקה Account מתוך NDB Properties:

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

בדרך כלל לא רוצים לאחזר את כל הישויות מסוג מסוים, אלא רק את אלה עם ערך ספציפי או טווח ערכים ספציפיים במאפיין מסוים.

אובייקטים של מאפיינים מעמיסים יתר על המידה חלק מהאופרטורים כדי להחזיר ביטויי סינון שאפשר להשתמש בהם כדי לשלוט בשאילתה: לדוגמה, כדי למצוא את כל ישויות החשבון שמאפיין מזהה המשתמש שלהן הוא בדיוק 42, אפשר להשתמש בביטוי

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

(אם אתם בטוחים שיש רק Account אחד עם userid, יכול להיות שתעדיפו להשתמש ב-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 או שווה לו.

שתיים מהפעולות האלה, != ו-IN, מיושמות כשילוב של האחרות, והן קצת מוזרות כמו שמתואר במאמר != ו-IN.

אפשר לציין כמה מסננים:

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

הפונקציה משלבת את ארגומנטי המסנן שצוינו ומחזירה את כל ישויות Account שהערך של userid שלהן גדול מ-40 או שווה לו וקטן מ-50.

הערה: כמו שצוין קודם, 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

שחזור המחלקה Article מתוך NDB Properties:

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

הערה: יכול להיות שתופתעו, אבל השאילתה הזו לא מחפשת ישויות Article שלא כוללות את perl כתג! היא מחפשת את כל הישויות עם לפחות תג אחד שלא שווה ל-perl. לדוגמה, הישות הבאה תיכלל בתוצאות, למרות שיש לה perl כאחד מהתגים שלה:

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

אבל כתובת ה-URL הזו לא תיכלל:

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 התוצאות לא חוזרות על עצמן: זרם התוצאות לא כולל ישות יותר מפעם אחת, גם אם ישות תואמת לשתי שאילתות משנה או יותר.

שאילתות לגבי מאפיינים חוזרים

המחלקות Article שמוגדרות בקטע הקודם משמשות גם כדוגמה לשאילתות של מאפיינים חוזרים. חשוב לציין שמסנן כמו

Article.tags == 'python'

משתמש בערך יחיד, למרות ש-Article.tags הוא מאפיין חוזר. אי אפשר להשוות מאפיינים חוזרים לאובייקטים של רשימה (Datastore לא יבין את זה), ומסנן כמו

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

הפעולה שמתבצעת שונה לחלוטין מחיפוש של Article ישויות שהערך של התגים שלהן הוא הרשימה ['python', 'ruby', 'php']: המערכת מחפשת ישויות שהערך tags שלהן (שנחשב לרשימה) כולל לפחות אחד מהערכים האלה.

התנהגות לא מוגדרת מתרחשת כשמבצעים שאילתה על ערך של None במאפיין חוזר. אל תעשו את זה.

שילוב של פעולות AND ו-OR

אפשר לקנן פעולות של AND ו-OR באופן שרירותי. לדוגמה:

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. AND עם OR בתוכו שווה ל-OR של כמה אופרנדים של AND שמוחל על האופרנדים המקוריים של AND, עם אופרנד יחיד של OR במקום האופרנד המקורי של AND. לדוגמה, AND(a, b, OR(c, d)) שווה ל-OR(AND(a, b, c), AND(a, b, d))OR
  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. שימוש בכלל מספר 1 באופרטורים IN ו-!=:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php',
             OR(tags < 'perl', tags > 'perl'))))
  2. שימוש בכלל מספר 2 ב-OR הפנימי ביותר שמוטמע בתוך AND:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         OR(AND(tags == 'php', tags < 'perl'),
            AND(tags == 'php', tags > 'perl'))))
  3. שימוש בכלל מספר 4 במאפיין OR שמוטמע בתוך מאפיין OR אחר:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php', tags < 'perl'),
         AND(tags == 'php', tags > 'perl')))
  4. שימוש בכלל מספר 2 על OR שמוטמעים בתוך AND:
    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'))

שימו לב: במקרה של חלק מהמסננים, הנורמליזציה הזו עלולה לגרום לפיצוץ קומבינטורי. קחו לדוגמה מסנן מסוג AND עם 3 סעיפים מסוג OR, כשכל אחד מהם כולל 2 סעיפים בסיסיים. אחרי הנורמליזציה, המסנן הזה הופך למסנן מסוג OR עם 8 סעיפים מסוג AND, כשכל אחד מהם כולל 3 סעיפים בסיסיים. כלומר, 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(), מאגר הנתונים דוחה שילובים מסוימים. בפרט, כשמשתמשים במסנן אי-שוויון, סדר המיון הראשון (אם יש כזה) חייב לציין את אותו נכס כמו המסנן. בנוסף, לפעמים צריך להגדיר אינדקס משני.

שאילתות לגבי ישות אב

שאילתות לגבי ישות אב מאפשרות לכם לבצע שאילתות עם מודל עקביות חזק במאגר הנתונים, אבל יש מגבלה של כתיבה אחת לשנייה לישויות עם אותה ישות אב. הנה השוואה פשוטה בין היתרונות והחסרונות של שאילתא לגבי ישות אב לבין שאילתת לא צאצאים, באמצעות נתונים של לקוחות והרכישות המשויכות שלהם במאגר הנתונים.

בדוגמה הבאה של ישות לא צאצאית, יש ישות אחת במאגר הנתונים לכל Customer, וישות אחת במאגר הנתונים לכל Purchase, עם 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()

במקרה כזה, מאגר הנתונים מציע תפוקת כתיבה גבוהה, אבל רק מודל עקביות הדרגתי. אם נוספה רכישה חדשה, יכול להיות שתקבלו נתונים ישנים. אפשר למנוע את ההתנהגות הזו באמצעות שאילתות של צאצאים.

ללקוחות ולרכישות עם שאילתות של צאצאים, עדיין יש לכם את אותה מבנה עם שתי ישויות נפרדות. החלק שמתייחס ללקוח זהה. עם זאת, כשיוצרים רכישות, כבר לא צריך לציין את KeyProperty() לרכישות. הסיבה לכך היא שכאשר משתמשים בשאילתות של צאצאים, קוראים למפתח של ישות הלקוח כשיוצרים ישות רכישה.

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

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

לכל רכישה יש מפתח, וגם ללקוח יש מפתח משלו. עם זאת, כל מפתח רכישה יכלול את המפתח של customer_entity. חשוב לזכור: הפעולה הזו תוגבל לכתיבה אחת בשנייה לכל ישות אב. הדוגמה הבאה יוצרת ישות עם ישות אב:

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 באותה ישות מסוג Contact. לדוגמה:

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 יש מאפיין country עם default='us', הדוגמה שלמעלה תחזיר רק אנשי קשר שהמדינה שלהם שווה ל-'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')

אפשר להשתמש ב-GenericProperty גם אם המודל שלכם לא מבוסס על Expando, אבל אם אתם רוצים לוודא שאתם משתמשים רק בשמות מאפיינים מוגדרים, אתם יכולים להשתמש גם במאפיין המחלקה _properties

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

או להשתמש ב-getattr() כדי לקבל אותו מהכיתה:

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

ההבדל הוא שב-getattr() נעשה שימוש ב'שם Python' של המאפיין, ואילו ב-_properties נעשה אינדוקס לפי 'שם מאגר הנתונים' של המאפיין. ההבדל ביניהם מתבטא רק אם הנכס הוגדר באמצעות משהו כמו

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

בדוגמה הזו, השם ב-Python הוא title, אבל השם של מאגר הנתונים הוא t.

הגישות האלה מתאימות גם לסידור תוצאות של שאילתות:

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

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

Query Iterators

במהלך ביצוע שאילתה, המצב שלה נשמר באובייקט איטרטור. (ברוב האפליקציות לא משתמשים בהם ישירות, בדרך כלל יותר פשוט לקרוא ל-fetch(20) מאשר לתפעל את אובייקט האיטרטור). יש שתי דרכים בסיסיות לקבל אובייקט כזה:

  • שימוש בפונקציה המובנית iter() של Python באובייקט Query
  • הפעלת השיטה iter() של האובייקט Query

הראשונה תומכת בשימוש בלולאת for Python (שקוראת באופן מרומז לפונקציה iter()) כדי להריץ לולאה על שאילתה.

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

הדרך השנייה, באמצעות ה-method‏ iter() של האובייקט Query, מאפשרת להעביר אפשרויות לאיטרטור כדי להשפיע על ההתנהגות שלו. לדוגמה, כדי להשתמש בשאילתה של מפתחות בלבד בלולאה for, אפשר לכתוב את הקוד הבא:

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

למאותחלי שאילתות יש שיטות שימושיות אחרות:

‏Method תיאור
__iter__() חלק מפרוטוקול האיטרטור של Python.
next() מחזירה את התוצאה הבאה או מעלה את החריגה StopIteration אם אין תוצאה כזו.

has_next() מחזירה True אם קריאה עוקבת ל-next() תחזיר תוצאה, False אם היא תגרום להעלאת StopIteration.

הפונקציה נחסמת עד שהתשובה לשאלה הזו ידועה, ומאחסנת את התוצאה (אם יש כזו) במאגר עד לאחזור שלה באמצעות next().
probably_has_next() בדומה ל-has_next(), אבל משתמש בקיצור דרך מהיר יותר (ולפעמים לא מדויק).

יכול להחזיר תוצאת חיובית שגויה (True כש-next() יחזיר למעשה StopIteration), אבל אף פעם לא תוצאה שלילית שגויה (False כש-next() יחזיר למעשה תוצאה).
cursor_before() מחזירה סמן שאילתות (query cursor) שמייצג נקודה ממש לפני התוצאה האחרונה שהוחזרה.

מועלית חריגה אם אין סמן זמין (במיוחד אם לא הועברה אפשרות השאילתה produce_cursors).
cursor_after() מחזירה סמן שאילתה שמייצג נקודה מיד אחרי התוצאה האחרונה שהוחזרה.

מועלית חריגה אם אין סמן זמין (במיוחד אם לא הועברה אפשרות השאילתה produce_cursors).
index_list() הפונקציה מחזירה רשימה של אינדקסים שמשמשים שאילתה שהופעלה, כולל אינדקסים ראשיים, מורכבים, מסוג kind ושל מאפיין יחיד.

סמני מיקום של שאילתות

סמן שאילתה הוא מבנה נתונים קטן ואטום שמייצג נקודת המשך בשאילתה. הוא שימושי להצגת דף תוצאות למשתמש בכל פעם, וגם לטיפול במשימות ארוכות שאולי צריך להפסיק ולהמשיך. דרך טיפוסית להשתמש בהם היא באמצעות שיטת fetch_page() של שאילתה. היא פועלת בדומה ל-fetch(), אבל היא מחזירה טריפלט (results, cursor, more). הדגל more שמוחזר מציין שיש כנראה עוד תוצאות. ממשק משתמש יכול להשתמש בו, למשל, כדי להסתיר את הלחצן או הקישור 'הדף הבא'. כדי לבקש דפים עוקבים, מעבירים את הסמן שמוחזר מקריאה אחת של 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 מיושם באמצעות השיטה 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=True אל iter(). כשהאיטרטור נמצא במקום הנכון, קוראים לשיטה 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)

הפעלת פונקציה לכל ישות (מיפוי)

נניח שאתם רוצים לקבל את הישויות Account שמתאימות לישויות Message שהוחזרו על ידי שאילתה. אפשר לכתוב משהו כמו:

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 שמופיעה למעלה, כי אפשר לבצע בה כמה פעולות בבו-זמניות. עם זאת, מכיוון שהקריאה get() ב-callback() עדיין סינכרונית, השיפור לא משמעותי. זה מקום טוב להשתמש בבקשות GET אסינכרוניות.

GQL

‫GQL היא שפה דמוית-SQL לאחזור ישויות או מפתחות מ-App Engine Datastore. התכונות של 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".
  • כדי להריץ שאילתות על מודלים שמכילים מאפיינים מובנים, אפשר להשתמש ב-foo.bar בתחביר GQL כדי להפנות למאפייני משנה.
  • ‫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() מבטלת את המגבלה או ההיסט שהוגדרו על ידי הסעיפים OFFSET ו-LIMIT של GQL. אין לשלב בין הסעיפים OFFSET ו-LIMIT של GQL לבין fetch_page(). שימו לב: המגבלה של 1,000 תוצאות שמוטלת על ידי App Engine על שאילתות חלה גם על היסט וגם על מגבלה.

אם אתם רגילים ל-SQL, כדאי להיזהר מהנחות שגויות כשמשתמשים ב-GQL. השאילתה ב-GQL מתורגמת ל-NDB's native query API. זה שונה ממיפוי אובייקטים יחסיים (Object-Relational mapper) רגיל (כמו SQLAlchemy או התמיכה במסד הנתונים של Django), שבו קריאות ה-API מתורגמות ל-SQL לפני שהן מועברות לשרת מסד הנתונים. שפת GQL לא תומכת בשינויים ב-Datastore (הוספות, מחיקות או עדכונים); היא תומכת רק בשאילתות.