ServiceStack: Update<T>(...) produces 'duplicate entry' - servicestack

I have tried reading the docs, but I don't get why the Update method produces a "Duplicate entry" MySQL error.
The docs says
In its most simple form, updating any model without any filters will update every field, except the Id which is used to filter the update to this specific record:
So I try it, and pass in an object, like below. A row with id 2 already exists.
using (var _db = _dbFactory.Open())
{
Customer coreObject = new Customer(...);
coreObject.Id = 2;
coreObject.ObjectName = "a changed value";
_db.Update<Customer>(coreObject); // <-- error "duplicate entry"
}
Yes, there are options using .Save and such, but what am I missing with the .Update? As I read it, it should use its Id property to update the row in the db, not insert a new row?

The issue with this method is that you're updating a generic object T but your Update API says to update the Concrete Customer type:
public void MyTestMethod<T>(T coreObject) where T : CoreObject
{
long id = 0;
using (var _db = _dbFactory.Open())
{
id = _db.Insert<T>(coreObject, selectIdentity: true);
if (DateTime.Now.Ticks == 0)
{
coreObject.Id = (uint)id;
_db.Delete(coreObject);
}
if (DateTime.Now.Ticks == 0)
{
_db.DeleteById<Customer>(id);
}
if (DateTime.Now.Ticks == 0)
{
coreObject.Id = (uint)id;
coreObject.ObjectName = "a changed value";
_db.Update<Customer>(coreObject);
}
}
}
Which OrmLite assumes that you're using a different/anonymous object to update the customer table, similar to:
db.Update<Customer>(new { Id = id, ObjectName = "a changed value", ... });
Which as it doesn't have a WHERE filter will attempt to update all rows with the same primary key.
What you instead want is to update the same entity, either passing in the Generic Type T or have it inferred by not passing in any type, e.g:
_db.Update<T>(coreObject);
_db.Update(coreObject);
Which will use OrmLite's behavior of updating entity by updating each field except for Primary Keys which it instead used in the WHERE expression to limit the update to only update that entity.
New Behavior in v5.1.1
To prevent accidental misuse like this I've added an Update API overload in this commit which will use the Primary Key as a filter when using an anonymous object to update an entity, so your previous usage:
_db.Update<Customer>(coreObject);
Will add the Primary Key to the WHERE filter instead of including it in the SET list. This change is available from v5.1.1 that's now available on MyGet.

Related

session.queryObjects does not support secondary types

Reading this https://chemistry.apache.org/docs/cmis-samples/samples/properties/index.html#retrieving-properties, I thought it would be possible to retrieve secondary types using queryObjects method, but it does not. For example, I'm trying to get cm:author from Alfresco, it returns null. Here is my piece of code:
OperationContext oc = OperationContextUtils.createMaximumOperationContext();
ItemIterable<CmisObject> results = session.queryObjects(task.getCmisType(), where, false, oc);
...
Object value = cmisObject.getPropertyValue("cm:author");
Am I missing something?
P.S: I'm using Chemistry 1.0.0, CMIS 1.1, Binding: Browser
UPDATE:
Okay I found something interesting, In order to retrieve cm:author, I have to reload the cmisObject to make it work:
results = session.queryObjects("cmis:document", "IN_FOLDER('" + folder.getId() + "')", false, oc);
results.each { it ->
object = session.getObject(it.getId());
author = object.getPropertyValue("cm:author");
if(author != null) {
println object.getId() + " => " + author;
}
Bug?
First make sure cm:author is what you want. That is not the person who created the document node in Alfresco. That is an editable property that anyone can set to anything, and by default it is null.
If what you want is the actual username of the person who created the document node, you should use cmis:createdBy which is mapped to alfresco's cm:creator property.
Assuming cm:author is definitely what you want, you have two choices regarding how to get it. First, you can get it from the object. But in order to get it from the object you must first fetch the object. Your query returns QueryResult objects, not CmisObjects.
So you should do something like:
ItemIterable<QueryResult> results = session.query(queryString, false);
for (QueryResult qResult : results) {
String objectId = "";
PropertyData<?> propData = qResult.getPropertyById("cmis:objectId");
if (propData != null) {
objectId = (String) propData.getFirstValue();
}
CmisObject obj = session.getObject(session.createObjectId(objectId));
// Dump the object here
System.out.println("Author: " + obj.getPropertyValue("cm:author");
}
Your second option would be to get the property value from the query result. Your ability to do this depends on the query you ran. The author property is defined on an aspect, so you must do a join in order to get it back. The query might look something like:
queryString = "select content.cmis:name, content.cmis:objectId, author.cm:author from cmis:document content JOIN cm:author author ON content.cmis:objectId = author.cmis:objectId WHERE content.cmis:objectId is not null AND author.cm:author = 'Jeff'";
If you use that query, then you can grab the author using the QueryResult, like this:
System.out.println("Author: " + qResult.getPropertyValueByQueryName("author.cm:author"));
Hopefully that explains the difference between fetching the value from a query result and fetching a property value from the object itself.

Field index for queries not updating when value set programmatically

My module creates a custom content item through the controller:
private ContentItem createContentItem()
{
// Add the field
_contentDefinitionManager.AlterPartDefinition(
"TestType",
cfg => cfg
.WithField(
"NewField",
f => f
.OfType(typeof(BooleanField).Name)
.WithDisplayName("New Field"))
);
// Not sure if this is needed
_contentDefinitionManager.AlterTypeDefinition(
"TestType",
cfg => cfg
.WithPart("TestType")
);
// Create new TestType item
var newItem = _contentManager.New("TestType");
_contentManager.Create(TestItem, VersionOptions.Published);
// Set the added boolean field to true
BooleanField newField = ((dynamic)newItem).TestType.NewField as BooleanField;
newField.Value = true;
// Set title (as date created, for convenience)
var time = DateTime.Now.ToString("MM-dd-yyyy h:mm:ss tt", CultureInfo.InvariantCulture).Replace(':', '.');
newItem.As<TitlePart>().Title = time;
return newItem;
}
The end result of this is a new TestType item with a field that's set to true. Viewing the content item in the dashboard as well as examining ContentItemVersionRecord in the database confirms that the value was set correctly.
However, queries don't seem to work properly on fields that are set in this manner. I found the record IntegerFieldIndexRecord, which is what I assume projections use to fill query result pages. On this, the value of TestField remains at 0 (false), instead of 1 (true).
Going to the content item edit page and simply clicking 'save' updates IntegerFieldIndexRecord correctly, meaning that the value is now picked up by the query. How can the record be updated for field values set programmatically?
Relevant section of migration:
SchemaBuilder.CreateTable(typeof(TestTypePartRecord).Name, table => table
.ContentPartRecord()
);
ContentDefinitionManager.AlterTypeDefinition(
"TestType",
cfg => cfg
.DisplayedAs("Test Type")
.WithPart(typeof(TitlePart).Name)
.WithPart(typeof(ContainablePart).Name)
.WithPart(typeof(CommonPart).Name)
.WithPart(typeof(IdentityPart).Name)
);
Edit: The fix for this is to manually change the projection index record whenever changing a field value, using this call:
_fieldIndexService.Set(testResultItem.As<FieldIndexPart>(),
"TestType", // Resolves as TestTypePart, which holds the field
"newField",
"", // Not sure why value name should be empty, but whatever
true, // The value to be set goes here
typeof(bool));
In some cases a simple contentManager.Publish() won't do.
I've had a similar problem some time ago and actually implemented a simple helper service to tackle this problem; here's an excerpt:
public T GetStringFieldValues<T>(ContentPart contentPart, string fieldName)
{
var fieldIndexPart = contentPart.ContentItem.As<FieldIndexPart>();
var partName = contentPart.PartDefinition.Name;
return this.fieldIndexService.Get<T>(fieldIndexPart, partName, fieldName, string.Empty);
}
private void SetStringFieldValue(ContentPart contentPart, string fieldName, IEnumerable<int> ids)
{
var fieldIndexPart = contentPart.ContentItem.As<FieldIndexPart>();
var partName = contentPart.PartDefinition.Name;
var encodedValues = "{" + string.Join("},{", ids) + "}";
this.fieldIndexService.Set(fieldIndexPart, partName, fieldName, string.Empty, encodedValues, typeof(string));
}
I've actually built this for use with MediaLibrary- and ContentPicker fields (they encode their value as string internally), so it might not be suitable for the boolean field in your example.
But it can't be that hard to implement, just look at the existing drivers and handlers for those fields.
There are 2 ways to fix this:
1) Ensure the newly created item is getting published by calling ContentManager.Publish() as Orchard.Projections.Handlers.FieldIndexPartHandler listens to the publish event to update the FieldIndexPartRecord
2) use IFieldIndexService to update FieldIndexPartRecord manually, see implementation of Orchard.Projections.Handlers.FieldIndexPartHandler to get in idea how to do this
Hope this helps.
:edit
Due to calling Create(...Published) the ContentManager.Published() won't do anything as the item is already considered published.
You can do the following to force the publish logic to run:
bool itemPublished = newItem.VersionRecord.Published;
// unpublish item first when it is already published as ContentManager.Publish() internally first checks for published flag and when set it aborts silently
// -> this behaviour prevents calling publish listeners
if (itemPublished)
_contentManager.Unpublish(newItem);
// the following call will result in calls to IContentHandler.Publishing() / IContentHandler.Published()
_contentManager.Publish(newItem);
or just create the item as a draft and publish it when everything is setup correctly.

Relation between Retieve AND Update in a Dynamic CRM PLUGIN

Is there a relation between Retrieve and Update in a Dynamic CRM Plugin?
For example if I am retrieving only one field:
Entity e = (Entity)service.Retrieve("EntityLogicalName", EntityGuid,
new ColumnSet(new string[] {"entityid"}));
Can I update another field in the Entity e that has NOT been retrieved?
For example:
e.Attributes["AnotherEntityField1] = "test1";
e.Attributes["AnotherEntityField2] = "test2";
service.update(e);
By NOT including all fields that to be updated in the Retrieve, may this cause some hidden issues?
Assuming, as it appears, that you are just retrieving the entity's primary key, entityid, you won't need to do the retrieve.
Entity e = new Entity("EntityLogicalName") { Id = EntityGuid };
e.Attributes.Add("AnotherEntityField1", "test1");
e.Attributes.Add("AnotherEntityField2", "test2");
service.Update(e);
If you are doing a retrieve to confirm the record exists you need to try/catch or use a retrieve multiple since a Retrieve will throw an exception if the record does not exist.
What you are trying to do is perfectly acceptable and won't cause any problems. Since you obtained the Entity instance via a Retrieve operation the required LogicalName and Id will be set correctly for an update.
Your code would need to read as below for adding new attributes not retrieved initially otherwise you'll get a KeyNotFoundException as the Entity type is just a wrapper over Dictionary<string,string>.
e.Attributes.Add("AnotherEntityField2","test2");
When you're trying to update an entity you don't need that an field exists in the attributes colletion, but to avoid the exception "The given key is not presented in the dictionary" is a good practise to check first if the Attributes Colletion contains the field you want to updated. If yes just update it otherwise you have to add it to the Attributes Colletion of the entity.
if(e.Attributes.Contains("AnotherEntityField1"))
{
e.Attributes["AnotherEntityField1"] = "test1";
}
else
{
e.Attributes.Add("AnotherEntityField1", "test1");
}
// now update operation

Linq queries and optionSet labels, missing formatted value

I'm doin a simple query linq to retrieve a label from an optionSet. Looks like the formatted value for the option set is missing. Someone knows why is not getting generated?
Best Regards
Sorry for the unclear post. I discovered the problem, and the reason of the missing key as formattedvalue.
The issue is with the way you retrieve the property. With this query:
var invoiceDetails = from d in xrmService.InvoiceSet
where d.InvoiceId.Value.Equals(invId)
select new
{
name = d.Name,
paymenttermscode = d.PaymentTermsCode
}
I was retrieving the correct int value for the option set, but what i needed was only the text. I changed the query this way:
var invoiceDetails = from d in xrmService.InvoiceSet
where d.InvoiceId.Value.Equals(invId)
select new
{
name = d.Name,
paymenttermscode = d.FormattedValues["paymenttermscode"]
}
In this case I had an error stating that the key was not present. After many attempts, i tried to pass both the key value and the option set text, and that attempt worked just fine.
var invoiceDetails = from d in xrmService.InvoiceSet
where d.InvoiceId.Value.Equals(invId)
select new
{
name = d.Name,
paymenttermscode = d.PaymentTermsCode,
paymenttermscodeValue = d.FormattedValues["paymenttermscode"]
}
My guess is that to retrieve the correct text associated to that option set, in that specific entity, you need to retrieve the int value too.
I hope this will be helpful.
Best Regards
You're question is rather confusing for a couple reasons. I'm going to assume that what you mean when you say you're trying to "retrieve a label from an OptionSet" is that you're attempting to get the Text Value of a particular OptionSetValue and you're not querying the OptionSetMetadata directly to retrieve the actual LocalizedLabels text value. I'm also assuming "formatted value for the option set is missing" is referring to the FormattedValues collection. If these assumptions are correct, I refer you to this: CRM 2011 - Retrieving FormattedValues from joined entity
The option set metadata has to be queried.
Here is an extension method that I wrote:
public static class OrganizationServiceHelper
{
public static string GetOptionSetLabel(this IOrganizationService service, string optionSetName, int optionSetValue)
{
RetrieveOptionSetRequest retrieve = new RetrieveOptionSetRequest
{
Name = optionSetName
};
try
{
RetrieveOptionSetResponse response = (RetrieveOptionSetResponse)service.Execute(retrieve);
OptionSetMetadata metaData = (OptionSetMetadata)response.OptionSetMetadata;
return metaData.Options
.Where(o => o.Value == optionSetValue)
.Select(o => o.Label.UserLocalizedLabel.Label)
.FirstOrDefault();
}
catch { }
return null;
}
}
RetrieveOptionSetRequest and RetrieveOptionSetResponse are on Microsoft.Xrm.Sdk.Messages.
Call it like this:
string label = service.GetOptionSetLabel("wim_continent", 102730000);
If you are going to be querying the same option set multiple times, I recommend that you write a method that returns the OptionSetMetadata instead of the label; then query the OptionSetMetadata locally. Calling the above extension method multiple times will result in the same query being executed over and over.

Add or replace entity in Azure Table Storage

I'm working with Windows Azure Table Storage and have a simple requirement: add a new row, overwriting any existing row with that PartitionKey/RowKey. However, saving the changes always throws an exception, even if I pass in the ReplaceOnUpdate option:
tableServiceContext.AddObject(TableName, entity);
tableServiceContext.SaveChangesWithRetries(SaveChangesOptions.ReplaceOnUpdate);
If the entity already exists it throws:
System.Data.Services.Client.DataServiceRequestException: An error occurred while processing this request. ---> System.Data.Services.Client.DataServiceClientException: <?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<code>EntityAlreadyExists</code>
<message xml:lang="en-AU">The specified entity already exists.</message>
</error>
Do I really have to manually query for the existing row first and call DeleteObject on it? That seems very slow. Surely there is a better way?
As you've found, you can't just add another item that has the same row key and partition key, so you will need to run a query to check to see if the item already exists. In situations like this I find it helpful to look at the Azure REST API documentation to see what is available to the storage client library. You'll see that there are separate methods for inserting and updating. The ReplaceOnUpdate only has an effect when you're updating, not inserting.
While you could delete the existing item and then add the new one, you could just update the existing one (saving you one round trip to storage). Your code might look something like this:
var existsQuery = from e
in tableServiceContext.CreateQuery<MyEntity>(TableName)
where
e.PartitionKey == objectToUpsert.PartitionKey
&& e.RowKey == objectToUpsert.RowKey
select e;
MyEntity existingObject = existsQuery.FirstOrDefault();
if (existingObject == null)
{
tableServiceContext.AddObject(TableName, objectToUpsert);
}
else
{
existingObject.Property1 = objectToUpsert.Property1;
existingObject.Property2 = objectToUpsert.Property2;
tableServiceContext.UpdateObject(existingObject);
}
tableServiceContext.SaveChangesWithRetries(SaveChangesOptions.ReplaceOnUpdate);
EDIT: While correct at the time of writing, with the September 2011 update Microsoft have updated the Azure table API to include two upsert commands, Insert or Replace Entity and Insert or Merge Entity
In order to operate on an existing object NOT managed by the TableContext with either Delete or SaveChanges with ReplaceOnUpdate options, you need to call AttachTo and attach the object to the TableContext, instead of calling AddObject which instructs TableContext to attempt to insert it.
http://msdn.microsoft.com/en-us/library/system.data.services.client.dataservicecontext.attachto.aspx
in my case it was not allowed to remove it first, thus I do it like this, this will result in one transaction to server which will first remove existing object and than add new one, removing need to copy property values
var existing = from e in _ServiceContext.AgentTable
where e.PartitionKey == item.PartitionKey
&& e.RowKey == item.RowKey
select e;
_ServiceContext.IgnoreResourceNotFoundException = true;
var existingObject = existing.FirstOrDefault();
if (existingObject != null)
{
_ServiceContext.DeleteObject(existingObject);
}
_ServiceContext.AddObject(AgentConfigTableServiceContext.AgetnConfigTableName, item);
_ServiceContext.SaveChangesWithRetries();
_ServiceContext.IgnoreResourceNotFoundException = false;
Insert/Merge or Update was added to the API in September 2011. Here is an example using the Storage API 2.0 which is easier to understand then the way it is done in the 1.7 api and earlier.
public void InsertOrReplace(ITableEntity entity)
{
retryPolicy.ExecuteAction(
() =>
{
try
{
TableOperation operation = TableOperation.InsertOrReplace(entity);
cloudTable.Execute(operation);
}
catch (StorageException e)
{
string message = "InsertOrReplace entity failed.";
if (e.RequestInformation.HttpStatusCode == 404)
{
message += " Make sure the table is created.";
}
// do something with message
}
});
}
The Storage API does not allow more than one operation per entity (delete+insert) in a group transaction:
An entity can appear only once in the transaction, and only one operation may be performed against it.
see MSDN: Performing Entity Group Transactions
So in fact you need to read first and decide on insert or update.
You may use UpsertEntity and UpsertEntityAsync methods in the official Microsoft Azure.Data.Tables TableClient.
The fully working example is available at https://github.com/Azure-Samples/msdocs-azure-data-tables-sdk-dotnet/blob/main/2-completed-app/AzureTablesDemoApplicaton/Services/TablesService.cs --
public void UpsertTableEntity(WeatherInputModel model)
{
TableEntity entity = new TableEntity();
entity.PartitionKey = model.StationName;
entity.RowKey = $"{model.ObservationDate} {model.ObservationTime}";
// The other values are added like a items to a dictionary
entity["Temperature"] = model.Temperature;
entity["Humidity"] = model.Humidity;
entity["Barometer"] = model.Barometer;
entity["WindDirection"] = model.WindDirection;
entity["WindSpeed"] = model.WindSpeed;
entity["Precipitation"] = model.Precipitation;
_tableClient.UpsertEntity(entity);
}

Resources