Breeze.js通过异步请求启动(加载和缓存)数据

时间:2014-06-02 19:05:41

标签: javascript entity-framework breeze single-page-application

我正在尝试用3个数据集填充我的Breeze缓存,为每个实体集创建一个单独的异步WebApi请求:

  • 实体1:人(原则)
  • 实体2:汽车
  • 实体3:SpeedingTicket

唯一的两个关系是:

  • 人有很多汽车
  • 人有很多SpeedingTickets

我服务MetaData来描述这些关系

问题是:

Breeze是否需要将Person(原则)加载到Cars和SpeedingTickets(dependents)之前的缓存中,以成功建立相关的外键关系? 或者它可以首先处理孤立的dependents加载,并在最后加载Person(原则)时动态地​​建立关联吗?

编辑:似乎订单无关紧要

EF关系:

    modelBuilder.Entity<Person>().HasKey(p => p.Id);

    modelBuilder.Entity<Person>()
        .HasMany(p => p.Tickets)
        .WithRequired(t => t.Person)
        .HasForeignKey(t => t.PersonId);

    modelBuilder.Entity<Person>()  
        .HasMany(p => p.Cars) 
        .WithRequired(c=>c.Person)
        .HasForeignKey(c=>c.PersonId);  

DataContext:检索人员,汽车,门票(每个远程和本地)的方法

var DataContext = function() {
    function configureBreezeManager() {
        breeze.NamingConvention.camelCase.setAsDefault();
        return new breeze.EntityManager("http://localhost:7071/breeze/data/");;
    }

    var EntityQuery = breeze.EntityQuery,
        manager = configureBreezeManager();

    var
        QUERY_ALL_PERSONS = EntityQuery.from('People'),
        QUERY_ALL_CARS = EntityQuery.from('Cars'),
        QUERY_ALL_TICKETS = EntityQuery.from('Tickets');

    var
        querySuccess = function(result) {
            console.log("Fetched: " + result.query.resourceName);
        },
        queryFail = function(jqXhr, textStatus) {
            console.log("failed " + textStatus);
        };

    var
        getPersons = function() {
            return manager.executeQuery(QUERY_ALL_PERSONS)
                .then(querySuccess).fail(queryFail);
        },

        getCars = function() {
            return manager.executeQuery(QUERY_ALL_CARS)
                .then(querySuccess).fail(queryFail);
        },
        getTickets = function() {
            return manager.executeQuery(QUERY_ALL_TICKETS)
                .then(querySuccess).fail(queryFail);
        },

        //LOCALS

        getPersonsLocal = function() {
            return manager.executeQueryLocally(QUERY_ALL_PERSONS);
        },
        getCarsLocal = function() {
            return manager.executeQueryLocally(QUERY_ALL_CARS);
        },
        getTicketsLocal = function() {
            return manager.executeQueryLocally(QUERY_ALL_TICKETS);
        };

    return {
        getPersons: getPersons,
        getCars: getCars,
        getTickets: getTickets,

        getPersonsLocal: getPersonsLocal,
        getCarsLocal: getCarsLocal,
        getTicketsLocal: getTicketsLocal

    };          
};

控制台输出功能:

function logLocalData() {
        var people = dc.getPersonsLocal(),
            cars = dc.getCarsLocal(),
            tickets = dc.getTicketsLocal();
        var pLen = people.length,
            cLen = cars.length,
            tLen = tickets.length;

        for (var p = 0 ; p < pLen; p++)
            console.log(people[p].name + " has " + people[p].cars.length + " car and " + people[p].tickets.length + " ticket");

        for (var t = 0 ; t < tLen; t++)
            console.log("ticket " + tickets[t].id + " belongs to person: " + (tickets[t].person));

        for (var c = 0 ; c < cLen; c++)
            console.log("car " + cars[c].id + " belongs to person: " + (cars[c].person));
}

以下是5个测试,每个测试加载以不同顺序链接的实体,最后一个测试将它们加载到异步中

TEST1:(OK)人们在家属之前加载(微风正确地关联所有3个实体) - 所需的结果

var dc = new DataContext();
dc.getPersons()
    .then(dc.getCars)
    .then(dc.getTickets)
    .then(function () {
        logLocalData();
    })
    Fetched: People 
    Fetched: Cars 
    Fetched: Tickets 
    Joe has 1 car and 1 ticket 
    Sam has 1 car and 1 ticket 
    Bob has 1 car and 1 ticket 
    ticket 0 belongs to person: [object Object] 
    ticket 1 belongs to person: [object Object] 
    ticket 2 belongs to person: [object Object] 
    car 0 belongs to person: [object Object] 
    car 1 belongs to person: [object Object] 
    car 2 belongs to person: [object Object] 

TEST 2:(FAIL)PEOPLE按照顺序加载后依次为:CARS,TICKETS,PEOPLE(注意TICKET&gt; PERSON关联 null ,但CAR&gt; PERSON没问题)

dc.getCars()
    .then(dc.getTickets)
    .then(dc.getPersons)
    .then(function () {
        logLocalData();
    })
Fetched: Cars
    Fetched: Tickets 
    Fetched: People
    Joe has 1 car and 0 ticket 
    Sam has 1 car and 0 ticket 
    Bob has 1 car and 0 ticket 
    ticket 0 belongs to person: null
    ticket 1 belongs to person: null
    ticket 2 belongs to person: null
    car 0 belongs to person: [object Object] 
    car 1 belongs to person: [object Object]
    car 2 belongs to person: [object Object] 

TEST 3:(FAIL)PEOPLE按顺序装入受抚养人:TICKETS,CARS,PEOPLE(注意CAR> PERSON关联 null ,但是TICKET&gt; PERSON没问题) - 与TEST 2 *

相反
dc.getTickets()
    .then(dc.getCars)
    .then(dc.getPersons)
    .then(function () {
        logLocalData();
    })
    Fetched: Tickets
    Fetched: Cars
    Fetched: People
    Joe has 0 car and 1 ticket
    Sam has 0 car and 1 ticket
    Bob has 0 car and 1 ticket
    ticket 0 belongs to person: [object Object] 
    ticket 1 belongs to person: [object Object] 
    ticket 2 belongs to person: [object Object]
    car 0 belongs to person: null 
    car 1 belongs to person: null 
    car 2 belongs to person: null 

TEST 4:(OK)PEOPLE按顺序加载2ND:TICKETS,PEOPLE,CARS(微风正确关联所有3个实体)

dc.getCars()
    .then(dc.getPersons)
    .then(dc.getTickets)
    .then(function () {
        logLocalData();
    })
Output is same as TEST 1

测试5:(失败)使用 Q.all 异步加载所有设置本来是我的首选方法,但它会根据服务器响应时间产生不可预测的结果每次通话。

Q.all([ dc.getTickets(), dc.getCars(), dc.getPersons()])
    .then(function () {
        logLocalData();
    }).catch(function () { console.log("error"); })
    Output produces random results (could be any from TEST 1, 2, 3) based on server response time for each call.

结论

  • 在2个家属之前加载原则时,所有关联都已正确制作
  • 在2个受抚养人之间加载原则时,所有关联都已正确制作
  • 在家属之后装载原则时,第二个 Dependent没有关联,并且是孤立的。

重申问题

这是预期的行为吗? Breeze是否需要在汽车和门票(家属)之前加载到缓存中的人(原则),以成功建立相关的外键关系?或者我的代码有问题吗?

这是一个简化的例子,我有更多的实体集加载到SPA中,并且显然不需要跟踪加载顺序和保持同步。

进一步的实验

我决定完全把注意力从Q.all / promises和查询执行上移开,因为这似乎把焦点从潜在问题上移开了。我还将数据最小化为三个实体类型中的每一个的一个对象。

所以在这里,我留下了以下内容:

  • 基于EF6上下文提供程序生成的MetaData 模型关系描述
  • 手动将实体创建为breeze cache
  • 人&#34; Jim&#34;应该有一辆车和一张票。

测试6:(确定)通知呼叫顺序 - 创建人员第一

mgr.metadataStore.fetchMetadata("http://localhost:7071/breeze/data/")
    .then(function() {

        mgr.createEntity('Person', { id: 1, name: 'Jim' });
        mgr.createEntity('Car', { id: 20, name: 'car', personId: 1 });
        mgr.createEntity('Ticket', { id: 30, personId: 1 });

    }).catch(function () { console.log("error"); })
   .done(function () {logLocalData();}) 

一切都很好了

    Jim(1) has 1 car and 1 ticket
    ticket 30 belongs to person: [object Object] 
    car 20 belongs to person: [object Object] 

测试7:(失败)通知调用顺序 - 人员创建最后,汽车对象未连接到人员

mgr.metadataStore.fetchMetadata("http://localhost:7071/breeze/data/")
    .then(function() {

        mgr.createEntity('Ticket', { id: 30, personId: 1 });
        mgr.createEntity('Car', { id: 20, name: 'car', personId: 1 });
        mgr.createEntity('Person', { id: 1, name: 'Jim' });

    }).catch(function () { console.log("error"); })
   .done(function () {logLocalData();})

汽车对象未连接到人

    Jim(1) has 0 car and 1 ticket
    ticket 30 belongs to person: [object Object]
    car 20 belongs to person: null

问题出在哪里?

  • a)我的模型关系配置? (一直在顶部)
  • b)我的MetaData? (由EFContextProvider自动生成) - 我可以按请求格式化它。
  • c)其他

此外,进一步的实验:更多地隔离代码。

  • 不再有WebAPI
  • 不再有EF生成的元数据
  • 不再有服务器代码
  • No Q / promises
  • Just Breeze,我的客户端脚本和我
  • 问题仍然存在

我手工创建了元数据,使用breeze的元数据助手非常简单。

好的,这是我的One脚本,参考 breeze &amp; breeze.metadata-helper ,仅此而已,问题依然存在:

    /********** METADATA GENERATOR **********/

    var MyMetaDataGenerator = function() {
        var keyGen = breeze.AutoGeneratedKeyType.Identity,
            namespace = 'genob.Model',
            helper = new breeze.config.MetadataHelper(namespace, keyGen);

        var DT = breeze.DataType, ID = DT.Int32;

        function createMetadataStore(serviceName) {
            var addType = function(type) { helper.addTypeToStore(store, type); };

            var store;

            store = new breeze.MetadataStore({
                namingConvention: breeze.NamingConvention.camelCase
            });
            helper.addDataService(store, serviceName);

            addPerson();
            addCar();
            addTicket();

            return store;

            function addPerson() {
                addType({
                    name: 'Person',
                    dataProperties: {
                        id: { type: ID },
                        name: { max: 50, nullOk: false }
                    },
                    navigationProperties: {
                        cars: { type: 'Car', hasMany: true },
                        tickets: { type: 'Ticket', hasMany: true }
                    }
                });
            }

            function addCar() {
                addType({
                    name: 'Car',
                    dataProperties: {
                        id: { type: ID },
                        name: { max: 50, nullOk: false },
                        personId: { type: ID, nullOk: false },
                    },
                    navigationProperties: {
                        person: 'Person',
                    }
                });
            }

            function addTicket() {
                addType({
                    name: 'Ticket',
                    dataProperties: {
                        id: { type: ID },
                        name: { max: 50, nullOk: false },
                        personId: { type: ID, nullOk: false },
                    },
                    navigationProperties: {
                        person: 'Person',
                    }
                });
            }

        }

        return {
            createMetadataStore: createMetadataStore
        };
    };

    /********** DATA CONTEXT **********/

    var DataContext = function () {

        var
            EntityQuery = breeze.EntityQuery,
            manager = configureBreezeManager();

        function configureBreezeManager() {
            var serviceName = "a/b",
                metaDataCreator = MyMetaDataGenerator(),
                store = metaDataCreator.createMetadataStore(serviceName),
                mgr = new breeze.EntityManager({
                    serviceName: serviceName,
                    metadataStore: store
                });
            return mgr;
        }

        var getManager = function() {
            return manager;
        };
        var
            getPersonsLocal = function() {
                return manager.executeQueryLocally(EntityQuery.from('Persons'));
            },
            getCarsLocal = function() {
                return manager.executeQueryLocally(EntityQuery.from('Cars'));
            },
            getTicketsLocal = function() {
                return manager.executeQueryLocally(EntityQuery.from('Tickets'));
            };

        return {
            getManager: getManager,

            getPersonsLocal: getPersonsLocal,
            getCarsLocal: getCarsLocal,
            getTicketsLocal: getTicketsLocal,
        };

    };

    ///////********** MY APP **********//////////

    var App = function() {
        var dc = new DataContext();

        function logLocalData() {
            var people = dc.getPersonsLocal(),
                cars = dc.getCarsLocal(),
                tickets = dc.getTicketsLocal();
            var pLen = people.length, cLen = cars.length, tLen = tickets.length;

            for (var p = 0; p < pLen; p++)
                console.log(people[p].name + "(" + people[p].id + ") has " + people[p].cars.length + " car and " + people[p].tickets.length + " ticket");

            for (var t = 0; t < tLen; t++)
                console.log("ticket " + tickets[t].id + " belongs to person: " + (tickets[t].person));

            for (var c = 0; c < cLen; c++)
                console.log("car " + cars[c].id + " belongs to person: " + (cars[c].person));
        }

        var run = function() {

            var mgr = dc.getManager();

            mgr.createEntity('Ticket', { id: 30, personId: 1 });
            mgr.createEntity('Car', { id: 20, personId: 1 });
            mgr.createEntity('Person', { id: 1, name: 'Jim' });

            logLocalData();
        };
        return {
            run: run
        };
    };

    App().run();

输出:注意CAR&gt; PERSON关系没有连线,(注意:如果我们先加载CAR,然后是TICKET,那么TICKET&gt; PERSON没有连线,第二个从属关系永远不会被连线)。

        Jim(1) has 0 car and 1 ticket 
        ticket 30 belongs to person: [object Object] 
        car 20 belongs to person: null 

但是,如果我们切换订单以插入第一个人:

    mgr.createEntity('Person', { id: 1, name: 'Jim' });
    mgr.createEntity('Ticket', { id: 30, personId: 1 });
    mgr.createEntity('Car', { id: 20, personId: 1 });

输出:连接所有关系(一切正常)

        Jim(1) has 1 car and 1 ticket 
        ticket 30 belongs to person: [object Object] 
        car 20 belongs to person: [object Object] 

问题出在哪里?

我已经消除了不可能的事情,只留下了不可能的事情,那么真相是什么呢?

2 个答案:

答案 0 :(得分:0)

修改

http://jsfiddle.net/G3mxq/4/

看起来问题不是Person必须最后,但是有两个没有反向属性的关联只能清理第一个。我从未使用过metadataGenerator,但即使不使用它也存在问题。

检查小提琴是否有重复,并将最靠近底部的创建汽车呼叫移动到创建实体呼叫列表中的任何其他位置。

直到它可以被解决,你可以先调用getPeople()。executeQuery(),然后在返回的promise上调用另外两个。

原始

我认为您的问题在于您没有正确使用Q.all(),但如果没有看到正在调用它的内容以及它是如何工作的话,很难说清楚。

function getData() {
    var promise = Q.all([ dc.getTickets(), dc.getCars(), dc.getPersons()]);

    return promise.then(querySuccess);

    function querySuccess() {
        // Check that everything has been returned
    }
}

只要您的所有三个电话都返回一个承诺,它应该等到所有电话都完成后再继续。

答案 1 :(得分:0)

Breeze不应该关心实体到达的顺序。如果你等到它们全部到达,它们都应该连线。确实有些不妥。

你的代码看起来很好,直到我偶然发现了记者函数logLocalData

您正在使用迭代OBJECT属性的JavaScript for

for (var p in people)
    console.log(people[p].name + " has " + people[p].cars.length + 
                " car and " + people[p].tickets.length + " ticket");

在你的情况下这种作品(否则你不会看到任何东西)。而且此错误无法解释您的结果。但这是不正确的。您希望for遍历ELEMENTS OF ANARRAY。

for (var p, len=people.length; p < len; p++)
    console.log(people[p].name + " has " + people[p].cars.length + 
                " car and " + people[p].tickets.length + " ticket");

或ES5数组的forEach方法

people.forEach(function(p) {
    console.log(p.name + " has " + p.cars.length + 
                " car and " + p.tickets.length + " ticket");
});

请尝试将catch添加到Q.all,然后告诉我们您的看法。

3个相关查询的DocCode测试

因为这是一种常见的情况,我添加了一个传递demonstration test to the DocCode sample。它使用Knockout,但这无关紧要。也许你会发现一些有启发性的东西。

/*********************************************************
* When run separate queries for Employee, Orders, EmployeeTerritories.
* Breeze will wire up their relationships.
* 
* These next tests are a response to the SO question
* http://stackoverflow.com/questions/24001496/breeze-js-priming-loading-and-caching-data-via-asynchronous-requests
*
* The tests verify the entity wiring after all queries have finished.
*
* Note that Employee has many Orders and many EmployeeTerritories
* which matches the SO question's model structure
*********************************************************/
asyncTest("Can run parallel queries and breeze will wire relationships", 5, function () {
    var em = newEm();
    var queries = make_Emp_Orders_EmpTerritories_Query(em);
    var all = [
        queries.eQuery.execute(),
        queries.etQuery.execute(),
        queries.oQuery.execute()
    ];
    Q.all(all)
    .then(function () { check_Emp_Orders_EmpTerritories(queries); })
    .catch(handleFail).finally(start);
});

asyncTest("Can chain queries, dependent first, and breeze will wire relationships", 5, function () {
    var em = newEm();
    var queries = make_Emp_Orders_EmpTerritories_Query(em);
    // Run dependent entity queries first starting with Orders
    queries.oQuery.execute()
    // then EmployeeTerritories
    .then(function () { return queries.etQuery.execute(); })
    // lastly the principal (Employee) query
    .then(function () { return queries.eQuery.execute(); })
    // now assert that everything is wired up
    .then(function () { check_Emp_Orders_EmpTerritories(queries); })
    .catch(handleFail).finally(start);
});

function make_Emp_Orders_EmpTerritories_Query(em) {
    var eQuery = EntityQuery.from("Employees").using(em)
            .where('EmployeeID', '==', 1); // trim to just Nancy
    var etQuery = EntityQuery.from("EmployeeTerritories").using(em);
    var oQuery = EntityQuery.from("Orders").using(em)
             .where('EmployeeID', '==', 1); // trim to just Nancy's orders 
    return {
        eQuery: eQuery,
        etQuery: etQuery,
        oQuery: oQuery
    };
}

function check_Emp_Orders_EmpTerritories(queries) {
    var emps           = queries.eQuery.executeLocally();
    var empTerritories = queries.etQuery.executeLocally();
    var orders         = queries.oQuery.executeLocally();

    equal(emps.length, 1, "should have one Employee (Nancy)");
    notEqual(orders.length, 0, "should have Orders");
    notEqual(empTerritories.length, 0, "should have EmployeeTerritories");

    var e1 = emps[0];
    var e1Name = e1.FirstName();
    var e1OrdersLen = e1.Orders().length;
    var e1EmpTerritoriesLen = e1.EmployeeTerritories().length;

    notEqual(e1OrdersLen, 0,
        "'{0}' has {1} Orders".format(e1Name, e1OrdersLen));
    notEqual(e1EmpTerritoriesLen, 0,
        "'{0}' has {1} EmployeeTerritories".format(e1Name, e1EmpTerritoriesLen));
};

请注意,断言会练习导航属性并从缓存中提取值。

随意处理链式承诺的顺序。没关系。