2011-11-05

RESTful Services - כיצד מיישמים בפועל? (2)

תזכורת: REST הוא סגנון ארכיטקטוני, המתואר כסט אילוצים שעל המערכת לציית להם. הוא מקדם שימוש נכון ומדוייק בפרוטוקול HTTP וה Web Standards. הוא "חי בהרמוניה עם ה Web" ולא רק "משתמש ב Web כאמצעי תעבורה". ארכיטקטורה ירוקה : )

קצת להכניס אתכם לסלנג של שועלי ה REST המשופשפים (לא הייתי מגדיר את עצמי ככזה):
  • ראשי התיבות REST מייצגים Representational state transfer
  • POX הוא Plain Old XML, על משקל POJO. על מנת לציין ש REST הוא XML פשוט על הנשלח על גבי [HTTP[1
  • ל REST אפשר לקרוא גם (WOA (Web Oriented Architecture - על מנת לתאר את הקשר ל SOA או (ROA  (Resource Oriented Architecture - על מנת לתאר את הקשר ל Resource-Based Distributed Systems.
  • WS-* מוקצה לחלוטין. עדיף למלמל "השם ירחם" כל פעם שמזכירים אותם ולהזכיר מיד שהם מפרים עקרונות רבים של HTTP ו ה Web (כמו למשל - ביצוע queries לקריאה בלבד ב POST).
  • שימוש רק ב URI ולא ב URL. יש הבדל קטן (URI הוא ללא ה filename), אבל מי שחי REST לרוב מקפיד לדייק.
זהו, עכשיו לא נביך את עצמנו בקרב המומחים, ואנו מוכנים לצלול לפרטים.


The REST Uniform Interface
REST מציג את החוקים הבאים:

שימוש ב URI כמתאר של resources
דוגמאות ל resources הם: instance של הזמנה, לקוח או פוסט בבלוג - המקביל ל instance של class ב OO.
כמה כללים צריכים להשמר: 
  • ה URL (אני אשתמש ב URI ו URL לסרוגין. סליחה) צריך לספק שקיפות על מבנה ה Resources, כלומר:
    • http://painting.org/france/paris/louvre/leonardo-da-vinci/mona-lisa   הוא URI טוב
    • http://painting.org/painting/UDF-444LR123   הוא תיאור לא מוצלח, כי אינו חושף שום פרט.
  • כל "/" מתאר בהכרח רמה היררכית של מבנה המשאבים.
  • יש להשתמש במקף ("-") ולא בקו תחתון ("_") להפרדת מילים. כדרג אגב ע"פ התקן (RFC 3986) החלק הרלטיבי של ה URL מוגדר כ case sensitive.
  • וכו'


מידול Resource-Based
חלק זה הוא בעל ההשפעה הגדולה ביותר על ארכיטקטורת המערכת, והוא לעיתים הסיבה מדוע מימוש REST אינו ישים במערכת קיימת ללא Refactoring מקיף.
כמו שציינתי בפוסט הקודם, בעזרת ה URI אני ניגש ל Resource ישירות ומבצע עליו פעולה. ה Resource אינו מגדיר מתודות (כמו service), אלא אני יכול לבצע עליו רק את פעולות ה HTTP הסטדרטיות:
  • GET = קריאת ה resource. מקביל לקריאות פונקצניונליות כגון getOrderDetails אולי getOwner או findBid.
  • PUT = במקור מתואר: פעולת rebind של resource. הוספת resource חדש או עדכון resource קיים. 
  • POST = שליחת מידע ("post") ל resource קיים. מקביל לקריאות פונקציונליות כגון executeOrder או updateOrder. שינוי ה state הקיים.
  • DELETE = מחיקת ה resource. ביצוע פעולת Delete על משאב Subscription הוא מה שהיינו מתארים ב WS כ unsubscribe().
עודכן בעקבות תיקון של אלון.

למי שמכיר HTTP, מוגדרות בו יותר מ 4 הפעולות הבסיסיות הנ"ל. לדוגמא: HEAD, TRACE, OPTIONS וכו'. הגדרת ההתנהגות שלהן היא קצת פחות ברורה ופתוחה לפרשנות של מפתח מערכת ה REST.

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

http://example.com/orders/2009/10/776654

אזי גם:
http://example.com/orders/2009/10
http://example.com/orders/2009
http://example.com/orders
http://example.com

צריכים להיות משאבים נגישים.

ובכן, על פניו היצמדות למודל זה נראית לא מעט טרחה! מה היתרונות שאני מקבל מהם:
  • Cache: ה Scale של האינטרנט מבוסס לחלוטין על קיומם של Caches. ה Cache יכול להיות באפליקציה, שרת ה Web (למשל IIS או Apache), רכיבי הרשת או רשת ה CDN (כמו Akamai שסיפרתי עליה כאן). הכלל מאוד פשוט: קריאות GET (וגם HEAD או OPTIONS) הן cached וקריאות POST, PUT וכו' מוסיפות dirty flag על ה cache של אותו resource. אם כתבתם אפליקציות רשת רבות ואינכם זוכרים שכתבתם קוד כזה - זה מובן. המימוש נעשה ע"י שרת האינטרנט וברמות שונות של ה network devices השונים. כולם מתואמים ומצייתים לאותם חוקים. תארו לכם איזה שיפור אתם מקבלים כאשר ה router דרכו עובר משתמש באוסטרליה מספק לו את התשובה לאפליקציה שלכם מתוך cache אוסטרלי איי שם במקום להעמיס על המערכת שלכם. כל זאת, מבלי שאתם כותבים שורת קוד בודדת.
    אפליקציות רבות נוהגות להשתמש ב POST תמיד (משיקולי אורך URL אפשרי, או שקר כלשהו לגבי אבטחה) וכך מאבדות את הייתרון המשמעותי הזה. מצד שני, אם הגדרתם קריאת GET שמשנה את ה State - אכלתם אותה: ה caches לא יהיו מעודכנים[2]
  • ציוד רשת כגון Proxies, Firewalls, Web Application Firewalls מנועי חיפוש ושירותי רשת שונים מכירים את כללי ה WEB / HTTP ופועלים לפיהם. אנו נהנה מאבטחה טובה יותר, פחות בעיות לגשת לשירותים שלנו, diagnostics משופרים, ביצועים וכו'. השיפור שיושג משימוש ב CDN למשל יהיה משמעותי יותר.
  • היכולת להשתמש ב hyperlinks כשפת referencing. זה נשמע ייתרון קטן, אבל אני יכול להחזיר בתשובה לקריאה link למשאב אחר, אותו לינק הוא יציב - ניתן לשמור אותו ולהשתמש אח"כ. הוא תמיד מעודכן. זהו כלי מאוד שימושי. דוגמאות: פעולת GET על הזמנה נותנת לי links ל100 פריטים. אני יכול לסקור ולקרוא רק את הפריטים שמעניינים אותי ומתי שמתאים לי. פעולת POST שמייצרת דו"ח (או שאילתא - ברמת ה data זה בערך אותו הדבר) מחזירה לי URL שאפשר לגשת אליו כל פעם שאני רוצה לקבל את הדוח.
  • נגישות / תפוצה רחבה: כל פלטפורמה, וכל שפת תכנות כמעט יכולה לגשת למערכת שלי בקלות ללא שימוש בספריות מיוחדות (מה שלא כ"כ נכון ל WS-*). ניתן לגשת בקלות מ JavaScript או Flash. ניתן אפילו להפעיל ידנית מה Browser (למי שקצת יותר טכני).
  • פשטות: אחרי שנכנסים לראש REST הוא לא קשה במיוחד. קל לתחזק ולהרחיב את המערכת.
שימוש ב Hypermedia לניהול שינויי state
טוב, זהו נושא קצת יותר מורכב וקצת שנוי במחלוקת. אין מחלוקת שהוא חלק הגדרת ה REST, אבל פעמים רבות בוחרים לדלג עליו ולא להשתמש בו מכיוון שהוא מורכב יותר למימוש.
עקרון זה אומר שאחרי שביצעתי פעולות (לדוגמא קריאת GET של הזמנה), הפעולות האחרות הרלוונטיות האפשריות יהיו חלק מתשובת ה GET. למשל, הנה תשובה אפשרית לקריאת ההזמנה:

<order self='http://example.com/orders/3321'>
<amount>23</amount>

<product ref='http://example.com/products/4554' />

<customer ref='http://example.com/customers/1234' />

<link rel='edit'

ref='http://example.com/order-edit/ACDB' />

</order>
אני מקבל תיאור מפורש של סט הפעולות בעזרתן אני יכול להמשיך: edit עם לינק מתאים, ואת הפרטים של המוצר והלקוח.

היתרונות הם:
  • על הלקוח להחזיק / לעקוב אחר URL יחיד (Entry Point) למערכת שלי. מכאן והלאה הוא יובל בעקבות פעולותיו.
  • קוד הלקוח יכול לדעת באופן דינמי מהן הפעולות האפשריות ולאפשר אותן. השרת מצידו יכול להרחיב ולצמצם את סט הפעולות, עם הזמן, כרצונו[3].
  • אינני צריך לבצע עוד rountrip בנוסח קריאת GET ל OrderDetails על מנת לקבל קישורים למוצר או הלקוח.
כפי שאמרתי, זה נושא מורכב (מורכב = סיבה טובה להזהר) - אך הרעיון מקורי ומעניין.

Self descriptive message
יש גם עניין של הודעות שמתארות את עצמן, כלומר קריאות ללא ידע מוקדם. לדעתי זה עוזר בעיקר ל visibility ו debug - עקרון שהייתי מגדיר כנכון אוניברסלית ולא רק ל REST.


טעויות נפוצות של מימושי REST

  • שימוש ב POST לביצוע פעולות read (היה צריך להיות GET) או כל פעולה שאינה "create new". העניין הוזכר כבר למעלה. תתי בעיות:
    • התעלמות מ Caches (הוזכר למעלה)
    • נסיון להעביר XML שכולל מידע / פעולות מעורבות או שלא קיימות בסט המצומצם. לדוגמא פרמטר POST בשם operation ואז חזרה לעולם ה Services הוא טעות נפוצה מאוד של מתחילים.
      • http://example.com/myapi?operation=findOrder&orderid=41245
  • בניית URI בעזרת Query Parameters (רמז: Query param אמור לשמש ל... Query)
      • http://example.com/customer?name=AMEX&objType=order&objectid=1112234
  • שימוש לא נכון או שיכפול יכולות שקיימות כבר ב HTTP. דברים כמו:
    • Status / Error Code. תזכורת: פרוטוקול HTTP מותיר להוסיף לשגיאה טקסט חופשי (= גוף ההודעה).
    • Cookies
    • Headers
    • MIME Types
  • נסיון לשמור Server Side State על כל client. בעיה אוניברסלית ל Web.
  • המנעות מהוספת לינקים לתשובה: אמנם שימוש בלינקים (hypermedia) לניהול כל ה state הוא עקרון שנוי במחלוקת, אך המנעות מלינקים בכלל - נשמעת טעות. לא טוב להסתמך על קוד ב client שמתאר את מבנה ה API לשוא וגם חבל ליצור קריאות מיותרות.
  • נסיון לבצע Implicit Transactions.
    במוקדם או במאוחר תזדקקו ל Transactions. הדרך המומלצת לטעמי הוא לייצר resource שמתאר את ה transaction. כל דרך אחרת לעשות זאת בצורה לא מפורשת שנתקלתי בה - נגמרה בכאב.

מקווה שנהנתם.


[1] על מנת לדייק זה לא חייב להיות XML, ועקרונות ה REST יכולים להיות מיושמים גם ללא HTTP - פשוט אפשר להשתמש בהרמוניה בפרוטוקול אחר ובאותה הרוח ש REST "מתלבש" על HTTP.

[2] יצא לי להיתקל במערכת "REST" דיי גדולה ומורכבת שלא הקפידה על הכלל והיו לה בעיות עם Caches לא מעודכנים. המפתחים שלה התחכמו והוסיפו HEADER לכל קריאות ה GET שציין שאסור לשמור את הקריאה ב Cache. בעולם ה Security קוראים לזה (SDoS (Self Denial of Service

[3] ניתן להסתכל על זה כ interface דינאמי. ב Web Services הייתי קורא WSDL וה IDE היה יוצר לי stub - שזה מאוד נחמד. מצד שני, לכאורה, לא הייתי יכול להגיב לשינויי Interface ללא שינויי קוד.
אני אומר לכאורה מכיוון שיש כמה דרכים לבצע זאת בכל זאת (בצורה קצת יותר מסורבלת)


---

לינקים רלוונטים:
http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api




9 תגובות:

  1. סקירה מעניינת, שבאמת תועיל אפילו לכאלה שאינם רק מתחילים עם REST.
    אני הייתי מגדיר את PUT ו-POST קצת אחרת:
    PUT - הגדרת resource חדש, או עדכון resource קיים.
    POST - להעביר מידע לעיבוד ע"י resource קיים. זה פעולה די כללית, שבן השאר יכולה לגרום ליצירת תתי-resources.

    השבמחק
  2. אתה צודק לגמרי: זו הגדרה נכונה יותר.

    תודה רבה.

    השבמחק
  3. אנונימי20/1/13 06:48

    שאלה קטנה: נגיד ולמשל האתר שלי מתבסס על RESTful. האתר מכיל לצורך העניין מערכת של משתמשים כאשר לכל משתמש יש פרופיל. כדי לערוך את הפרופיל הוא יגש ל GET users/6 למשל ולאחר שיעשה submit לטופס הוא בעצם יגש ל PUT users/6. השאלה שלי היא: איך ניתן לוודא שהמשתמש באמת יכול לעשות את זה ומחובר לאתר בתור ID = 6? בוודאי שאפשר בפשטות לוודא לפי cookies בתחילת הפונקציה שאליה מועברת קריאת ה PUT, אך לפי מה שהבנתי (וממה שאתה גם כן כתבת) זה לא באמת מימוש אמיתי של RESTful, אם כך, מה עלי לעשות? אני יודע שאם מדובר על RESTful שמאפשר גם למפתחים חיצוניים לעבוד עליו בפשטות (למשל זה של טוויטר), אז משתמשים ב OAuth (שגם איתו אין לי ידע איך לעבוד). אבל מה אם מדובר על API כזה שגישה למפתחים זה לא חלק ממעייניו? שרק צריך לבדוק אם המשתמש אכן מחובר ואם אכן ערך את הפרופיל של עצמו.

    השבמחק
  4. אנונימי20/1/13 08:44

    עדכון קטן: אני חושב שפספסתי משהו בכל העניין של ה REST API. כאשר יוצרים REST API - המטרה היא אך ורק למפתחים צד שלישי? כלומר, אם ארצה שמשתמש יערוך את הפרופיל שלו, לעשות את זה פשוט רגיל עם טופס HTML שמכיל מתודת POST או GET ואז בצד שרת פשוט לעשות משהו בסגנון של "אם פוסט['שדה_חבוי'] == אמת אז פונקציה_לעדכון_נתונים()"? בהתחלה חשבתי על ליצור REST API לאתר עצמו בלי אפשרות לתת פיתוח למפתחים צד שלישי, אבל כנראה (לפי מה שהבנתי) ש REST API פשוט נועדה למפתחים צד שלישי ותו לא. אם כל מה שאמרתי נכון: האם כדאי ליצור מעיין סוג של REST API שרק מייצא public data מהמסד נתונים (בלי כל מיני פעולות שדורשות אימות משתמש)? כך שאוכל לקרוא את התוכן בעזרת JS. תודה.

    השבמחק
    תשובות
    1. היי אנונימי,

      REST היא טכניקה הן לAPI חיצוני והן ל API פנימי - באותה המידה. סביר מאוד להשתמש ב REST API עבור ה UI שלך בעבודה מול ה Server בלי לחשוף את זה לאף אחד.

      מה שאתה שואל זה על Authentication (איך מוודאים מי המשתמש) ו Authorization (מה מותר למשתמש לעשות) - בהחלט חלק רלוונטי למימוש REST. יש דרכים שונות לעשות Authentication, בניהן NTLM, Kerberos, SAML ו OAuth, כאשר האחרון רלוונטי יותר לתקשורת בין שרתים ולא בין client ל Server.

      הנה דרך פשוטה לביצוע אימות המשתמש שנקראת Form-Based authentication:
      בכניסה למערכת שלך אתה מבקש מהמשתמש user/password ומאמת אותו, יוצר לו session ושולח מספר זיהוי ארוך (שקשה מאוד לנחש) כ Cookie למשתמש. בכל קריאת REST אתה מקבל את ה Cookie, מוצא את ה session לראות איזה משתמש זה ומפעיל Authorization Logic לראות מה מותר לו לעשות.
      ה Authorization Logic יכולה להיות טבלה בבסיס הנתונים שמקשרת משתמש (או קבוצת משתמשים) לאובייקט (או סוג אובייקט) והפעולות שהם יכולים לבצע על אובייקט זה. למשל: create/ read/ delete וכו'. מה שנקרא (Access Control List (ACL.

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

      אני מקווה שהצלחתי לענות.


      ליאור

      מחק
  5. אנונימי2/12/14 15:09

    היי
    מאמר יפה.
    לא התייחסת לנושא של גירסאות. למשל: http://example.com/v1/orders
    אינסטגרם למשל משתמשים בזה (http://instagram.com/developer/endpoints/) בעוד חברות אחרות לא משתמשות...
    תוכל להרחיב על יתרונות וחסרונות?

    השבמחק
  6. אנונימי15/9/16 14:25

    הי ליאור,
    במאמר הראשון נאמר "על מנת לבצע שאילתה על כל ההזמנות בשנת 2009 של לקוח AMEX אני אבצע קריאת HTTP GET ל URL:
    http://example.com/orders?year=2009&customer=AMEX
    האובייקט הוא orders, אני מבצע קריאה ושולח פרמטרים ל Query בשם year ו customer.
    כמובן שאני לא יכול לשלוח מה שבא לי - רק מה שהוגדר ע"י ה שAPI ומתועד ב API Documentation."

    לעומת זאת תחת "טעויות נפוצות של מימושי REST" במאמר השני נאמר "ניית URI בעזרת Query Parameters (רמז: Query param אמור לשמש ל... Query)
    http://example.com/customer?name=AMEX&objType=order&objectid=1112234"

    מה אני מפספס פה?
    תודה ירון

    השבמחק
  7. הי ליאור,
    גיליתי את הבלוג שלך לאחרונה ונהנה מכל פוסט.

    ירון, גם אני תהיתי בקריאה ראשונה ובשניה הבנתי (אם אכן הבנתי נכון)
    הכוונה ש-URI מייצג RESOURCE והטעות היא ליצור URI שאמור לייצג RESOURCE אבל נראה כחו שאילתא. ה-URI בדוגמא הוא לא באמת שאילתא

    השבמחק
  8. אנונימי17/12/19 10:39

    המון תודה. מאוד עשה לי סדר.

    השבמחק