BreezeJS: Why aren't deletions made in Before/AfterSaveEntitiesDelegate propagated back to the client? - breeze

I am writing a simple event planning web app (using BreezeJS/Entity Framework) - users create a tournament entity, and ask the server to generate one or more suggested plans (just one for the purposes of this post).
Whenever the user clicks "generate plan", the tournament (including lots of details needed to generate a plan) should be submitted to the server, the server should delete any existing plan, generate a new one, and the client-side model should be updated.
A perfect fit for a named save, I thought!
The problem is the last step: updating the client-side model. The plan entity added by the server appears as expected in the client, but the deletion is ignored. I.e. the client ends up with both the new and the old plan!
Here's my named save:
[Note: The description and code in this question omits a lot of irrelevant details (like 20 properties and entity types) to keep the size of the question down]
[HttpPost]
public SaveResult MyNamedSave(JObject saveBundle)
{
_contextProvider.BeforeSaveEntitiesDelegate = RecalculatePlan;
return _contextProvider.SaveChanges(saveBundle);
}
private Dictionary<Type, List<EntityInfo>> RecalculatePlan(Dictionary<Type, List<EntityInfo>> arg)
{
// See https://stackoverflow.com/questions/14517945/using-this-context-inside-beforesaveentity:
var readonlyContext = new PontifexContext();
foreach (var eventInfo in arg[typeof(Tournament)])
{
var tournament = (Tournament)eventInfo.Entity;
var deletePlan = readonlyContext.Plans.First(p => p.TournamentId == tournament.Id);
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(deletePlan, EntityState.Deleted););
var addPlan = new Plan {TournamentId = tournament.Id, };
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(addPlan, EntityState.Added););
}
}
Am I trying to use named-saves for something they're not meant to do (i.e. deleting and adding entities)?
PS: I tried doing an explicit addition and save using both readonlyContext and _contextProvider.Context, but that really didn't work.
EDIT:
If I try to explicitly delete the old plan from the DB like below, nothing happens:
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(deletePlan, EntityState.Deleted););
// Add this:
context.PlanEntries.Remove(deletePlan);
context.SaveChanges();
I'm guessing it's because _contextProvider.Context already has the old plan in cache, so deleting it "behind its back" (i.e. using another context) doesn't make a difference.
If I then try removing it using _contextProvider.Context, I get a weird duplicate-entry error from the framework.
I'm at my wits' end!
EDIT 2:
Here's the data in the save request and response, as logged by IEs developer tools.
Request first:
{
"entities": [
{
"Id": 1,
"EventName": "Test Tournament",
"EventTime": "2015-03-21T20:00:00.000Z",
"entityAspect": {
"entityTypeName": "Tournament:#Pontifex.Model",
"defaultResourceName": "Tournaments",
"entityState": "Unchanged",
"originalValuesMap": { },
"autoGeneratedKey": {
"propertyName": "Id",
"autoGeneratedKeyType": "Identity"
}
}
}
],
"saveOptions": { }
}
The server then deletes the existing Plan entry (Id=10), and adds a new (Id=11), which I verified using a SELECT directly in the DB. That is good.
But the response is:
[
{
"$id": "1",
"$type": "Pontifex.Model.Tournament, Pontifex.Server",
"Id": 1,
"EventName": "Test Tournament",
"EventTime": "2015-03-21T20:00:00.000",
"Plans": [
{
"$id": "17",
"$type": "Pontifex.Model.Plan, Pontifex.Server",
"Id": 11,
"TournamentId": 1,
"Tournament": { "$ref": "1" }
}
],
"BoardPlan": null
}
]
In this response, the deleted entity never appears, so the client understandably leaves it in its model.
The added Plan (Id 11) does appear, and is integrated in the client model.
BUT: judging from sbelinis answer to Server added object showing as added in client after save changes, the fact that the added Plan appears may be a lucky coincidence:
In your particular example, the new entity made into the save because it happened to be related to the entity of the BeforeSaveEntity method, but you should not rely on it.
But sbelinis example of how to properly add an entity appears incomplete (e.g. it refers to a local variable saveMapAdditions which isn't used elsewhere)

OK, I figured out how to work around this!
I still can't get the deletion reflected back into the client cache...BUT if my server-side code ALSO removes the deleted entity from all relations, the removals WILL be reflected back, and the entity will disappear from the client-side model.
The updated code (I've added the statement tournament.Plans.Remove(deletePlan)):
[HttpPost]
public SaveResult MyNamedSave(JObject saveBundle)
{
_contextProvider.BeforeSaveEntitiesDelegate = RecalculatePlan;
return _contextProvider.SaveChanges(saveBundle);
}
private Dictionary<Type, List<EntityInfo>> RecalculatePlan(Dictionary<Type, List<EntityInfo>> arg)
{
// See http://stackoverflow.com/questions/14517945/using-this-context-inside-beforesaveentity:
var readonlyContext = new PontifexContext();
foreach (var eventInfo in arg[typeof(Tournament)])
{
var tournament = (Tournament)eventInfo.Entity;
var deletePlan = readonlyContext.Plans.First(p => p.TournamentId == tournament.Id);
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(deletePlan, EntityState.Deleted););
// Workaround: Remove the deleted plan from all relations:
tournament.Plans.Remove(deletePlan);
var addPlan = new Plan {TournamentId = tournament.Id, };
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(addPlan, EntityState.Added););
}
}
Of course, if you search the client local cache for Plan entities, I suspect the deleted plan will still appear, so it's not perfect. But it works for me!

Related

How to insert a record with a Relationship

Apologies, if this question is obvious, but I can't seem to find sufficient documentation. I might be lacking knowledge with restful methodologies. How do I store a record with a relationship?
I have a place. I want to store posts of users made to this place. So a place can have many posts. A post belongs to one place.
I'm using Aqueduct 3.0 Pre-Release.
I have following models:
place.dart
class Place extends ManagedObject<_Place> implements _Place {}
class _Place {
#primaryKey
int id;
#Column(unique: true)
String name;
ManagedSet<Post> posts;
}
post.dart
import 'package:places_api/model/place.dart';
import 'package:places_api/places_api.dart';
class Post extends ManagedObject<_Post> implements _Post {}
class _Post {
#primaryKey
int id;
#Column()
String text;
#Relate(#posts)
Place place;
}
I try to save the post, but there is only the possibility to store a place object, and not a place_id. Obviously below post query does not work, as there is only a values.place object, and not a values.place_id property. Is it intended to load the place, and then store all the posts to it?
Also without relationship, I can't store the place_id, as it seems that Aqueduct treats the _ as something special. Can't I use database properties, that have an underscore?
Is there any example that explains this?
#Operation.post()
Future<Response> createPost() async {
final body = request.body.asMap();
final query = new Query<Post>(context)
..values.place_id = body['place_id']
..values.text = body['text'];
final insertedPost = await query.insert();
return new Response.ok(insertedPost);
}
Currently I'm sending following body as POST:
{
"place_id": 1,
"text": "My post here"
}
To following URL: http://localhost:8888/posts
Would it be better to send something like this?
{
"text": "My post here"
}
To URL: http://localhost:8888/place/1/posts
Then fetch the place first, and store the post to it?
When represented as JSON, a relationship is always a list or an object. In your case:
{
"text": "text",
"place": {
"id": 1
}
}
This allows client application parsing code to remain consistent - a related object is always an object, never a synthetic field (e.g., place_id). The underlying database does name the column place_id by joining the relationship name and its primary key name with an underscore, but that's an implementation detail that isn't exposed through the API.
When inserting an object, foreign keys are inserted because they are a column in the table. Therefore, you can write your operation method as so:
#Operation.post()
Future<Response> createPost(#Bind.body() Post post) async {
final query = new Query<Post>(context)
..values = post;
final insertedPost = await query.insert();
return new Response.ok(insertedPost);
}
If you were to use the example JSON and this code, you'd get this SQL query: INSERT INTO _post (text, place_id) VALUES ('text', 1).
When inserting the 'has' side of a relationship, you have to insert the related objects as a separate query. An update/insert query will only set/insert values on a single table. If it makes sense for your API, you may want to POST the following place JSON:
{
"name": "Place",
"posts": [
{"text": "text"}
]
}
Your code to insert this object graph might look like:
await context.transaction((t) async {
final q = Query<Place>(t)..values = body;
final insertedPlace = await q.insert();
await Future.wait(body.posts, (p) async {
final postQuery = Query<Post>(t)
..values = p
..values.place.id = insertedPlace.id;
return postQuery.insert();
});
});
Couple of other small notes: asMap has been removed and replaced with as<T> or decode<T>. You also do not need an #Column annotation if you aren't adding any flags. All fields declared in the table definition type are database columns. Transient fields are declared in the ManagedObject subclass, and can be annotated with #Serialize() if you want them to be a part of the API surface.

How to push deletions to local breeze caches?

If I'm using breezejs and one client deletes a record from their cache and it saves to the server, then another client comes along with that record still in their cache, how do I get the second client's cache to update and remove the record that has been hard deleted?
It's a good question and we don't have a really great answer. We refer to these as 'ghost' entities, and the approach we have taken to them in the past is that if you requery for an entity ( or entities) by id, and they are not returned then you can safely remove them from the entityManager. (The code below has not been tested, but should give you the idea);
function checkIfDeleted(entityManager, entities) {
origEntities = entities.slice(0);
var q = EntityQuery.fromEntities(entities);
entityManager.executeQuery(q).then(function(data) {
var foundEntities = data.entities;
foundEntities.forEach(function(e, ix) {
if (entities.indexOf(e)) {
origEntities.splice(ix,1)
}
});
if (origEntities.length > 0) {
origEntities.forEach(function(e) {
entityManager.removeEntity(e);
});
}
});
}

breeze EntityManager: how to attach plain javascript object

I have custom OData action that is called from my client. The results from this action are a list of JSON objects which need to be merged back into the Breeze cache as entities. How do I convert a JSON object into a Breeze entity and merge that entity back into the entityManager's cache? Some code:
$http.post('/odata/MyEntityType/MyCustomAction/', {
'someData': JSON.stringify(element1),
'SomeOtherData': JSON.stringify(element2)
})
.success(function (results) {
//results.value is an array of *MyEntityType* JSON objects. `//Is there a way to convert these to breeze entities?`
});
Some things I have tried:
manager.importEntities("MyEntityType", theJsonForAnEntity); //just stabbing in the dark here
manager.createEntity("MyEntityType", theJsonForAnEntity); //error: A MergeStrategy of 'Disallowed' does not allow you to attach an entity when an entity with the same key is already attached"
createEntity won't work
Sorry, Jeremy, but that isn't going to work. Here's a test that shows why:
// Failing test
test("merge customer date into existing cached entity using `createEntity`", function () {
var em = newEm();
// Create an unchanged entity in cache as if it had been queried
em.createEntity('Customer', {
CustomerID: dummyCustID,
CompanyName: 'Foo Co',
ContactName: 'Ima Kiddin'
}, UNCHGD); // creates the entity in the Unchanged state
// Try to merge some changes into that entity using createEntity.
var changes = {
CustomerID: dummyCustID,
CompanyName: 'Bar Co',
}
var cust = em.createEntity('Customer', changes,
UNCHGD, breeze.MergeStrategy.OverwriteChanges);
ok(cust.entityAspect.entityState.isUnchanged(), "cust should be 'Unchanged'");
// using Knockout; it's simpler if using Angular
equal(cust.CompanyName(), 'Bar Co', "Company name should be updated by merge'");
equal(cust.ContactName(), 'Ima Kiddin', "Contact name should be unchanged after merge'");
});
The third assert fails because the createEntity method overwrites every property, not just the one data value of interest (CompanyName in this case). That means that ContactName is null.
importEntities won't work either
For the same reason. When you import an entity, you import the entire entity, not just some portion of it. So that too would wipe out the ContactName in this example.
Manual merge
I think if you want to blend the results with the entity in cache, you'll have to do that by hand. You'll have to iterate over the results and update the cached equivalents.
Imagine that the changes variable above is result of your POST. The following test does pass:
// Passing test
test("merge customer date into existing cached entity", function () {
var em = newEm();
// Create an unchanged entity in cache as if it had been queried
em.createEntity('Customer', {
CustomerID: dummyCustID,
CompanyName: 'Foo Co',
ContactName: 'Ima Kiddin'
}, UNCHGD); // creates the entity in the Unchanged state
// Merge some changes into that entity
// Imagine that `changes` came from your server as a result of your POST
var changes = {
CustomerID: dummyCustID,
CompanyName: 'Bar Co',
}
// First find the customer in cache.
// Here I'm assuming it will always be there; you should be careful
var cust = em.getEntityByKey('Customer', changes.CustomerID);
// using Knockout; it's a little simpler in Angular
for (var p in changes) { cust[p](changes[p]); }
cust.entityAspect.setUnchanged(); // cuz the previous updates changed the state to "Modified"
ok(cust.entityAspect.entityState.isUnchanged(), "cust should be 'Unchanged'");
equal(cust.CompanyName(), 'Bar Co', "Company name should be updated by merge'");
equal(cust.ContactName(), 'Ima Kiddin', "Contact name should be unchanged after merge'");
});
Feature Request
I can imagine a future in which Breeze could automate this for you with all of the appropriate error checks. If you think such a "JSON entity merge" feature would be a valuable enhancement to Breeze, please add that to User Voice. I would be quick to agree but we can't add features unless people want them.
I think you're onto something with the manager.createEntity approach. Try specifying a merge strategy (the default is "Disallowed" which is causing problems for your use case):
manager.createEntity("MyEntityType", thePocoEntityFromTheCustomODataAction, breeze.MergeStrategy.PreserveChanges);
Merge strategies: http://www.breezejs.com/sites/all/apidocs/classes/MergeStrategy.html
createEntity: http://www.breezejs.com/sites/all/apidocs/classes/EntityManager.html#method_createEntity

BreezeJS - Using expand

I am querying the server to get an entity with expand
function _loadIncidents() {
var deffered = Q.defer(),
queryObj = new breeze.EntityQuery().from('Incidents').expand(['Deployments', 'IncidentComments', 'DTasks', 'ExtendedProperties', 'IncidentEvents']);
dataRepository.fetchEntitiesByQuery(queryObj, true).then(function (incidents) {
var query = breeze.EntityQuery.from("DTasks"),
incidentIds = dataRepository.getEntitiesByQuerySync(query);
deffered.resolve();
}, function(err) {
deffered.reject(err);
});
return deffered.promise;
};
I am getting the results and all is fine, how ever when I query breeze cache to get the entities - I am getting empty collection. So when using expand does the expanded entities are added to the cache?
Yes the related entities identified in the expand should be in cache ... if the query is "correct" and the server interpreted your request as you intended.
Look at the payload of the response from the first request. Are the related entities present? If not, perhaps the query was not well received on the server. As a general rule, you want to make sure the data are coming over the wire before wondering whether Breeze is doing the right thing with those data.
I do find myself wondering about the spelling of the items in your expand list. They are all in PascalCase. Are they these the names of navigation properties of the Incident type? Or are they the names of the related EntityTypes? They need to be former (nav property names), not the latter.
I Had problem with the navigation property - as I am not using OData webapi not using EF , there is problem with the navigation properties so for the current time i just wrote
Object.defineProperty(this, 'Deployments', {
get: function () {
return (this.entityAspect && this.entityAspect.entityManager) ?
this.entityAspect.entityManager.executeQueryLocally(new breeze.EntityQuery("Deployments").
where('IncidentID', 'eq', this.IncidentID)) :
[];
},
set: function (value) { //used only when loading incidents from the server
if (!value.results) {
return;
}
var i = 0,
dataRepository = require('sharedServices/dataRepository');
for (i; i < value.results.length; i++) {
dataRepository.addUnchangedEntity('Deployment', value.results[i]);
}
},
enumerable: true
});

Sproutcore datasources and creating new records with relationships

I'm trying to get my head around datasources and related models in sproutcore and am getting no where fast was wondering if anyone could maybe help me understand this all bit better.
Basically I have two related models Client and Brand, Clients can have many Brands and Brands can have a single Client, I have defined my models correctly and everything is pulling back as expected. The problem I'm having is working out how to create a new Brand and setup its relationship.
So on my Brand controller I have a createBrand method like so:
var brand = DBs.store.createRecord(DBs.Brand, {
title: this.get('title')
}, Math.floor(Math.random()*1000000));
brand.set('client', this.get('client'));
MyApp.store.commitRecords();
So as this is a new Brand I randomly generate a new ID for it (the second argument to createRecord). This is calling my createRecord in my datasource to create the new Brand, and then it also calls the updateRecord for the client.
The problem I'm having is that the clientUpdate is being passed the temporary (randomly generated id) in the relationship. How should I be structuring my creating of the new brand? Should I be waiting for the server to return the newly created brands ID and then updating the client relationship? If so how would I go about doing this?
Thanks
Mark
Right, after sitting in the sproutcore IRC channel and talking to mauritslamers he recommended creating a framework to handle all the server interactions for me manually.
So I setup a framework called CoreIo, which contains all my models, store and data source.
The data source is only used for fetching records from the server ie:
fetch: function(store, query) {
var recordType = query.get('recordType'),
url = recordType.url;
if (url) {
SC.Request.getUrl(CoreIo.baseUrl+url)
.header({ 'Accept': 'application/json'})
.json()
.notify(this, '_didFetch', store, query, recordType)
.send();
return YES;
}
return NO;
},
_didFetch: function (response, store, query, recordType) {
if (SC.ok(response)) {
store.loadRecords(recordType, response.get('body'));
store.dataSourceDidFetchQuery(query);
} else {
store.dataSourceDidErrorQuery(query, response);
}
},
Then the CoreIo framework has creation methods for my models ie:
CoreIo.createBrand = function (brand, client) {
var data = brand,
url = this.getModelUrl(CoreIo.Brand);
data.client_id = client.get('id');
SC.Request.postUrl(url)
.json()
.notify(this, this.brandDidCreate, client)
.send(data);
};
CoreIo.brandDidCreate = function (request, client) {
var json = request.get('body'),
id = json.id;
var ret = CoreIo.store.pushRetrieve(CoreIo.Brand, id, json);
var brand = CoreIo.store.find(CoreIo.Brand, id);
if (ret) {
client.get('brands').pushObject(brand);
}
};
I would then call into these 'actions' to create my new models which would setup the relationships as well.

Resources