Breeze.js – משב רוח רעננה בפיתוח אפליקציות ניהול מידע

7 בינואר 2014

אין תגובות

Breeze.js הינה ספריית Javascript המאפשרת ניהול מידע, בעיקר באפליקציות הנקראות Rich Client Applications, כדוגמת SPA (Single Page Applications), המשתמשים בכל מבול הספריות המכונות MV*. Breeze.js מביא עימו מספר יכולות:

  • גרף ישויות – מחזיק גרף של אובייקטים Client-Side.
  • הורדת Metadata – של הישויות מהשרת, אין צורך בהגדרת ישויות בצד ה – Client
  • מעקב שינויים – שומר בזכרון פעולות CRUD שמתבצעות ב – Client
  • תשתית תשאול – המאפשרת לבצע שאילתות דמויות .Linשמירה – יכולת לשמור חבילת שינויים בפניה אחת לשרת.

בניגוד להרבה ספריות MV*, ל- Breeze יש גם רכיב .NET בצד השרת (!) המאפשר אינטגרציה עם תשתיות ORM כמו Entity Framework.

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

נבנה שירות המספק לנו שירותי CRUD ללקוחות Northwind ונדגים איך Breeze משתלב בצד השרת ובצד ה – Client.

צד שרת

נבנה אתר MVC מסוג Web Api. נוסיף Entity Framework, נצביע לבסיס נתונים מקומי של Northwind. אפשר לבחור רק את טבלאות Customers ו – Orders. עד כאן הכל רגיל, לא קשור לנושא שלנו.

כאמור, ל – Breeze יש רכיב .NET בשרת שמבצע אינטגרציה עם תשתית ה – Web Api ועם Entity Framework. ניתן להוריד דרך NuGet חבילה ל – Breeze Web Api. יירדו ספריות ותיווצר מחלקה חדשה בתיקיית AppStart בשם BreezeWebApiConfig, המכילה את התבנית של קריאה לשירות Web Api התומך ב – Breeze.

image

נבנה מחלקה חדשה, נקרא לה NorthwindRepository. מחלקה זו, לפי עיקרון ה- Repository, חושפת לנו פעולות שאפשר לעשות מול מקור מידע, תוך החבאת הטכנולוגיה, שבמקרה זה היא .EF כדי לעשות אינטגרציה עם Breeze, יורשים ממחלקה מיוחדת של Breeze –EFContextProvider, העוטף ב – Generics את המחלקה שיורשת מ –DbContext )שחולל אוטומטית כשהוספנו EF). מחלקה זו, EFContextProvider, מספקת פונקציונליות המאפשרת ליירט בקשות ל – EF ולבצע שליפת metadata, ולידציה, Formatting ועוד.

public class NorthwindRepository : EFContextProvider<NorthwindIBEntities>

{

    public NorthwindRepository()

    {


    }


    public DbQuery<Customer> Customers

    {

        get

        {

            return (DbQuery<Customer>)Context.Customer;

        }

    }


    public DbQuery<Order> Orders

    {

        get

        {

            return (DbQuery<Order>)Context.Order;

        }

    }



}

נוסיף כעת Controller (Add -> New Controller), נבחר ב – Empty Api Controller. כדי לעשות אינטגרציה ל – Controller עם Breeze, שמים Attribute ברמת המחלקה, שהינו Filter המאפשר Formatting JSON בשני הכיוונים, תקשורת בפורמט של OData ועוד. נוסיף פונקציונליות – משתנה מסוג NorthwindRepositoryשזה עתה בנינו, מתודה לחשיפת Metadata, מתודה להבאת לקוחות והזמנות ומתודה לשמירת שינויים:

[BreezeController]

   public class NorthwindController : ApiController

   {

       private readonly NorthwindRepository repository;


       public NorthwindController()

       {

           repository = new NorthwindRepository();

       }


       [HttpGet]

       public string Metadata()

       {

           return repository.Metadata();

       }


       [HttpPost]

       public SaveResult SaveChanges(JObject saveBundle)

       {

           return repository.SaveChanges(saveBundle);

       }


       [HttpGet]

       public IQueryable<Customer> Customers()

       {

           return repository.Customers;

       }


       [HttpGet]

       public IQueryable<Order> Order()

       {

           return repository.Orders;

       }

   }



כעת אם נפתח דפדפן ונגלוש ל – http://localhost:xxxx/breeze/Northwind/Customers, אנו אמורים לקבל את רשימת הלקוחות.

הערה: יש לבדוק תאימות ה – URL לתבנית המוגדרת במחלקה BreezeWebApiConfig:

image

Client

ניצור דף HTML ונוסיף לינקים ל – Breeze. בנוסף יש בדף פקדים לתצוגה ולביצוע פעולות CRUD, ורשימה נוספת של הזמנות של לקוח מסויים.




<!DOCTYPE html>

<html>

<head>

    <title></title>

    <style>

        .selected {

            background-color: lightgreen;

        }


        #wrap {

            column-count: 2;

        }

    </style>


</head>

<body>

    <div style="display: table; margin-right: auto; margin-left: auto;">

        <button type="button" style="width: 100px; height: 50px" onclick="createCustomer();">Add</button>

        <span>Company Name:</span>

        <input id="newCompanyName" type="text" />

        <button type="button" style="width: 100px; height: 50px" onclick="save();">Save</button>

        <button type="button" style="width: 100px; height: 50px" onclick="getCustomerOrders();">Orders</button>

    </div>

    <br />

    <br />


    <div style="width: 40%; float: left">

        Customers:

        <ul id="customersList">

        </ul>



    </div>


    <div style="width: 20%; float: left">

        Customer Orders:

        <ul id="customerOrders">

        </ul>


    </div>


<script src="../Scripts/jquery-1.8.2.js"></script>
   1:

   2:     <script src="../Scripts/jquery-ui-1.8.24.min.js">

   1: </script>

   2:     <script src="../Scripts/q.js">

   1: </script>

   2:     <script src="../Scripts/breeze.debug.js">

   1: </script>

   2:     <script src="../app/northwind.js">

</script>


</body>

</html>

נוסיף קובץ JS בשם northwind ובו נכתוב את כל המנגנון של Breeze. האובייקט המרכזי הינו entityManager שבעצם דרכו נעשות כל הפעולות. הבה נסקור את השימושים בו:

  • איתחול – יוצרים מופע של entityManager ומספקים URL של השירות שיצרנו קודם. יש הרבה אופציות לקונפיגורציה. מודגם גם האפשרות לשלוף metadata מהשרת (לא חייבים את זה, זה נעשה אוטומטית).
var serviceName = 'http://localhost:21511/breeze/Northwind';

   breeze.config.initializeAdapterInstances({

       modelLibrary: "backingStore"

   });


   var manager = new breeze.EntityManager(serviceName);

   manager.metadataStore.fetchMetadata(serviceName, sq, err);

  •  Query – פונקציה הפונה לשרת למתודה Customers. ניתן להרכיב שאילתה בסגנון Linq, המתורגם לפורמט של OData. אם נבחן את ה – URL שנוצר נוכל לראות זאת. מכיון שהשרת מחזיר IQueryable של כל הלקוחות, ניתן לשנות את השאילתה הבסיסית ולאכוף את הסינון שהגדרנו. כאשר המידע חוזר, הוא נשמר בזיכרון ב – entityManager. הרשומה נעטפת באובייקט המכיל את המידע של הרשומה (backingStore) וגם אובייקט הנקרא entityAspect המכיל את ה – metadata של הרשומה. כעת ניתן לעשות פעולות של הוספה, עידכון ומחיקה בזיכרון של ה – entityManager, ללא קריאה לשרת.
    function getAllCustomers() {

           var query = breeze.EntityQuery

                   .from("Customers")

                   .orderBy("CompanyName")

                   //.expand("Order")

                   .take(20);

    
    

           return manager.executeQuery(query);

       }

1

ניתן כמובן גם לעשות Paging. יש גם אופציה להביא את סך הרשומות (Inline Count). בדוגמה מביאים לקוחות המתאימים למחרוזת חיפוש:

function getCustomerPage(skip, take, searchText) {

       var query = breeze.EntityQuery

               .from("Customers")

               .orderBy("CompanyName")

               .skip(skip).take(take)

               .inlineCount(true);

       if (searchText) {

           query = query.where("CompanyName", "contains", searchText);

       }


       return manager.executeQuery(query);

   }

הערה – כל השאילתות של Breeze הינם אסינכרוניות, ולכן התבנית של קבלת המידע הינה בעזרת promise, על ידי שימוש ב – then, כמו בדוגמה הבאה:

image

  • הוספה של לקוח – כאשר ממלאים שם לקוח ולוחצים Add, ה – entityManager מוסיף רשומת לקוח לרשימה בזיכרון. לאובייקט entityAspect יש מאפיין בשם entityState המכיל ערכים של Added, Modified ו- .Deleted הרשומה שהוספנו תהיה Added. אין פניה לשרת בשלב זה.
function createCustomer(initialValues) {


        return manager.createEntity('Customer', initialValues);

    }

  • שינוי לקוח – בדרך כלל כשעובדים עם ספריה נוספת שעושה Binding של המידע לפקדים – השינויים הם אוטומטיים. כאן, לצורכי הדגמה, נעשה את זה ידנית. כאשר משנים ערך, אנחנו שולפים את הישות מהזכרון של entityManagerלפי שם, ועושים השמה לערך החדש – אוטומטית ה – entityState מקבל ערך של Modified.
    function textChanged(input) {

    
    

        //Delete

        if (input.value.length === 0) {

            var customer = app.dataservice.getCustomerLocally(originalValue);

            app.dataservice.removeCustomer(customer[0]);

        }

        else//Modify

        {

            currentEditedCustomer = app.dataservice.getCustomerLocally(originalValue);

            if (currentEditedCustomer.length > 0) {

                currentEditedCustomer[0].CompanyName = input.value;

            }

    
    

        }

    }

ל – entity Manager יש גם מנגנון של Observable, וניתן להירשם לאירוע של שינוי:

function subscribeChanges(callback) {

       manager.entityChanged.subscribe(callback);

   }

  • מחיקת לקוח – בדומה לשינוי, רק הפעם אנו שולפים את הרשומה וקוראים ידנית לשינוי entityState ל – Deleted.
    image
  • שמירה – כאשר רוצים לעדכן את בסיס הנתונים בשרת עם כל השינויים שבצענו בצד Client-Side, לוחצים על Save ובודקים אם יש שינויים ב – entityManager. ניתן לקרוא בנקודה זו לפונקציה getChanges ולבחון את כל השינויים שנעשו בזיכרון. אם יש שינויים, entityManager קורא אוטומטית לשרת, למתודה SaveChanges. המתודה מקבלת אובייקט מסוג JObject (מספריית Newtonsoft) המכיל את כל השינויים שבוצעו, ומעדכן את בסיס הנתונים.
    function saveChanges() {

           var changes = manager.getChanges();

           if (manager.hasChanges())

           {

    
    

               manager.saveChanges()

                   .then(saveSucceeded)

                   .fail(saveFailed)

           }

           else

           {

               alert("Nothing to save");

           };

       }

צד שרת:

[HttpPost]

       public SaveResult SaveChanges(JObject saveBundle)

       {

           return repository.SaveChanges(saveBundle);

       }

    • שליפת רשומות קשורות – כאשר יש קשר אחד לרבים בין ישויות , Entity Framework מחולל עוד מאפיין לכל צד של הקשר. לישות שמייצג את היחיד (Customer) נוסף מאפיין מסוג ICollection המייצג את כל הרשומות הקשורות.
      image

לישות המייצגת את הרבים בקשר (Order) נוסף מאפיין מסוג האב.
image

ניתן לשלוף את כל ההזמנות של לקוח בשני דרכים:

1) Eager Loading – בשיטה זו כאשר שולפים את הלקוחות, שולפים גם את כל ההזמנות של הלקוח על ידי שימוש ב – expand:

function getAllCustomers() {

       var query = breeze.EntityQuery

               .from("Customers")

               .orderBy("CompanyName")

               //.expand("Order")

               .take(20);


       return manager.executeQuery(query);

   }

2) Lazy Loading – באשר רוצים לשלוף את ההזמנות של לקוח מסויים על ידי שימוש ב – loadNavigationProperty:

function getOrders(customer) {

       if (customer) {

           customer.entityAspect.loadNavigationProperty("Order").then(viewOrders);

       }

       else {

           var query = breeze.EntityQuery

                   .from("Orders")

                   .take(50);


           return manager.executeQuery(query);

       }

   }

  •  Cache – כאמור, ה – entityManager שומר את כל המידע בזיכרון. אם רוצים לבצע שאילתות על מידע שהבאנו כבר מהשרת אין צורך ללכת שוב לשרת, עובדים עם המידע בזיכרון על ידי שימוש ב – executeQueryLocally
    function getCustomerLocally(companyName) {

            var query = new breeze.EntityQuery()

                .from("Customer")

            .where("CompanyName", "contains", companyName);

            return manager.executeQueryLocally(query);

    
    

        }

סיכום

Breeze הינה תשתית חזקה מאד המאפשרת ביצוע שאילתות וניהול מידע ב – Client Side.

Breeze משתלב יפה עם ספריות MV* אחרות, בין היתר Knockout, Backbone ו – Angular. נקודות ההתממשקות מאד פשוטות, כך שרשימת הספריות הולכת וגדלה.

קריאה עמוקה יותר בנושא ניתן למצוא כאן.

להורדת ה- Solution שמכיל דוגמאות אלו כאן.

 

שאלות נוספות בתחום פיתוח ל- Web? כנסו לפורום שלנו בעברית והתייעצו עם מיטב מומחי הקהילה.

 

הפוסט נכתב ע"י עוזי רונן, יועץ בכיר בקבוצת היועצים של מיקרוסופט (MCS) העוסק בייעוץ בתחומי ארכיטקטורה ופיתוח ושימש בעבר כמנהל פיתוח וארכיטקט במטריקס.

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *