In short, I want to associate arbitrary key/value pairs with the objects of a Core Data entity, on an iPad app.
My current solution is to have a to-many relationship with another entity that represents a single pair. In my application, I have:
Entry <--->> ExtraAttribute
where ExtraAttribute has properties key and value, and the key is unique to the ExtraAttribute's Entry.
Although the code to deal with this is slightly complicated, it is acceptable. The real problem comes with sorting.
I need to sort those Entries that have a given ExtraAttribute by that attribute. Using the SQL store, it is apparently impossible for Core Data itself to sort the Entries by the value of the associated ExtraAttribute with a given key. (Frustrating, since this is possible with the other stores, and trivial in SQL itself.)
The only technique I can find is to sort the entries myself, then write a displayOrder attribute back to the store, and have Core Data sort by the displayOrder. I do that with the following class method on Entry. (This uses a some methods and global functions not shown, but hopefully you can get the gist. If not, ask and I will clarify.)
NSInteger entryComparator(id entry1, id entry2, void *key) {
NSString *v1 = [[entry1 valueForPropertyName:key] description];
NSString *v2 = [[entry2 valueForPropertyName:key] description];
return [v1 localizedCompare:v2];
}
#implementation Entry
...
// Unified builtin property and extraAttribute accessor;
// expects human-readable name (since that's all ExtraAttributes have).
- (id)valueForPropertyName:(NSString *)name {
if([[Entry humanReadablePropertyNames] containsObject:name]) {
return [self valueForKey:
[Entry propertyKeyForHumanReadableName:name]];
} else {
NSPredicate *p = [NSPredicate predicateWithFormat:
#"key = %#", name];
return [[[self.extraAttributes filteredSetUsingPredicate:p]
anyObject] value];
}
}
+ (void)sortByPropertyName:(NSString *)name
inManagedObjectContext:(NSManagedObjectContext *)moc {
BOOL ascending = [Entry propertyIsNaturallyAscending:name];
[Entry sortWithFunction:entryComparator
context:name ascending:ascending moc:moc];
[[NSUserDefaults standardUserDefaults]
setObject:name
forKey:#"entrySortPropertyName"];
}
// Private method.
+ (void)sortWithFunction:(NSInteger (*)(id, id, void *))sortFunction
context:(void *)context
ascending:(BOOL)ascending
moc:(NSManagedObjectContext *)moc {
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:#"Entry" inManagedObjectContext:moc];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
NSError *error;
NSArray *allEntries = [moc executeFetchRequest:request error:&error];
[request release];
if (allEntries == nil) {
showFatalErrorAlert(error);
}
NSArray *sortedEntries = [allEntries
sortedArrayUsingFunction:sortFunction context:context];
int i, di;
if(ascending) {
i = 0; di = 1;
} else {
i = [sortedEntries count]; di = -1;
}
for(Entry *e in sortedEntries) {
e.displayOrder = [NSNumber numberWithInteger:i];
i += di;
}
saveMOC(moc);
}
#end
This has two major problems:
It's slow, even with small data sets.
It can take an arbitrarily large amount of memory and hence crash with large data sets.
I'm open to any suggestions that are easier than ripping out Core Data and using SQL directly. Thanks so much.
EDIT Thank you for your answers. Hopefully this will clarify the question.
Here is a typical data set: There are n Entry objects, and each one has a distinct set of key/value pairs associated with it. Here I am listing the key/value pairs under each entry:
Entry 1:
Foo => Hello world
Bar => Lorem ipsum
Entry 2:
Bar => La dee da
Baz => Goodbye cruel world
Here I want to sort the entries by any of the keys "Foo", "Bar", or "Baz". If a given entry doesn't have a value for the key, it should sort like an empty string.
The SQLite store cannot sort by an unknown key using -valueForUndefinedKey:; attempting to do so results in an NSInvalidArgumentException, reason keypath Foo not found in entity <NSSQLEntity Entry id=2>.
As noted in the documentation, only a fixed set of selectors will work with sort descriptors using the SQL store.
EDIT 2
Suppose there are three instances E1, E2, and E3 of my entity, and the user attaches the custom properties 'Name' and 'Year' to each of these instances. Then we might have:
E1 Bob 2010
E2 Alice 2009
E3 Charles 2007
But we wish to present these instances to the user, sorted by any of these custom properties. For example, the user might sort by Name:
E2 Alice 2009
E1 Bob 2010
E3 Charles 2007
or by Date:
E3 Charles 2007
E2 Alice 2009
E1 Bob 2010
and so on.
First question is, why do you need to store the sort in the database? If you are alway sorting in the key property, just use a sort descriptor whenever you need to access them in a sorted order.
Second question, why are you writing your own sort routine?
This design seems rather complicated. I understand the need for arbitratary storage of key value pairs, I designed a similar system in my book. However I am unclear as to the need for sorting those values nor the need for a custom sort routine such as this one.
If you could explain the need behind the sorting I could probably suggest a better strategy.
Also, I would highly recommend looking into the two methods -valueForUndefinedKey: and -setValue: forUndefinedKey: as a cleaner solution to your issue. That would allow you to write code like:
[myObject valueForKey:#"anythingInTheWorld"];
[myObject setValue:someValue forKey:#"anythingInTheWorld"];
and follow proper Key-Value Coding rules.
Update
The -valueForUndefinedKey: design is only for use in code, it is not for use accessing the store. I am still a little unclear with your goals.
Given the following model:
Entity <-->> Property
In this design, Property has two attributes:
Key
Value
From here you can access any property on Entity via -valueForUndefinedKey: because under the covers, Entity will go out and fetch the associated Property for that key. Thus you get dynamic values on your Entity.
Now the question of sorting. With this design, you can sort directly on SQLite because you are really sorting on the Property entity. Although I am still unclear as to the final goal of the sorting. What value does it have? How will it be used?
Update: Design reconsidered
The last design I proposed was wrong. On deeper reflection, it is simpler than I proposed. Your goal can be accomplished with the original Entity <-->> Property design. However there is a bit more work to be done in the -setValue: forKey: method. The logic is as follows:
External code called -setValue: forKey: on an Entity.
The -setValue: forKey: method attempts to retrieve the Property.
If the Property exists then the value is updated.
If the Property does not exist then a Property is created for each Entity with a default value set (assumed to be an empty string).
The only performance hit is when a new key is introduced. Other than that it should work without any performance penalties.
Related
Hi in one of my application,I have an array which contains a group of NSMutableDictionary objects. The dictionary object have three key-value pairs as like below
company
product
quantity
And array having many number of objects. Here now by using different add buttons I am adding these dictionary objects to the array. Even while adding objects to array i am checking whether any duplicate objects are available or not using NSNotFound method. As such below
if([Array indexOfObject:dicObject] == NSNotFound)
{
[Array addObject:dicObject];
}
Here it is working fine in few cases, But it's not working in other cases.I will explain with one example :
For example i have one dicobject in array with following key value pairs
company:XYZ Product:ABC Quantity:2
Now for example I want to add one more dic object with the same above key value pairs. That time obviously it won't add because already same product is available in array.
This is valid condition.
Exceptional Case: For example I want to add one more product with following values
Company:XYZ Product:ABC Quantity:6
At this case this product is adding into the array without any error. But my concern is i don't want to add this into the array again only the quantity have to update, because company and product name both are same so. So can you please show me the way to handle this scenario.
You could use indexOfObjectPassingTest: to know if a similar dictionary is already present in the array.
This may look something like this:
NSMutableArray *arr = // your array
NSDictionary *dicObject = // your object
NSUInteger indexOfDicObject = [arr indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop)
{
return ([obj[#"company"] isEqualToString:dicObject[#"company"]] &&
[obj[#"product"] isEqualToString:dicObject[#"product"]]);
}];
if (indexOfDicObject == NSNotFound)
{
[arr addObject:dicObject];
}
else
{
NSNumber *quantity = arr[indexOfDicObject][#"quantity"];
arr[indexOfDicObject][#"quantity"] = #([quantity intValue] + [dicObject[#"quantity"] intValue]);
}
I made the following assumptions:
the company value is a NSString;
the product value is a NSString;
the quantity value is an integer, stored in a NSNumber.
See also trojanfoe's answer, which is better if you can replace your dictionaries by classes.
I think you need to change tack; first create a custom object to hold your company, product and quantity and ensure you implement the isEqual: and hash methods.
Then simply store your custom objects within an NSMutableSet object, which will ensure that duplicates cannot exist.
Your custom object will now become your principle Model object for the app (i.e. provide the 'M' in MVC, the design pattern upon which Cocoa and Cocoa Touch apps are based) and you will find that it will be reused over and over as the app grows.
Here is my sitch. Let's say that I have Company and Employee object types, with company having to-many Employee's. I create a category for Company and add a method
-(Employee *)getNextEmployee {
return (Employee *)self.employees[self.currentEmployeeIndex++];
}
I realize that I can't access an NSSet by an index, but since it's a category I can't add an NSArray instance variable either. This method is being called frequently, so creating an NSArray for each call would be highly inefficient.
Again, please ignore specifics here, it's just an example.
Edit: Scratch my previous answer suggesting the use of the allObjects property of NSSet. Thinking about it made me realize allObjects is not guaranteed to return the array on the same order every time.
Instead I suggest you use a linked list. Create a previousEmployee property for your Employee class and every time you create a new Employee, using some logic you assign the previous employee. It could be based on the date of entry:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Employee"];
request.fetchLimit = 1;
NSSortedDescriptor *sortDescriptor = [sortDescriptorWithKey:#"entryDate" ascending:NO];
// Do further setup...
Employee *latestEmployee = // fetch result...
Employee *newEmployee = // create new employee
newEmployee.previousEmployee = latestEmployee;
There's no getting around having an array if you want ordering (implied by next). The way to have an array economically is to make a NSManagedObject subclass and add an array property to it. Update it when you do adds/removes from the set.
Both #danh and #pedro.m gave great answers. I ended doing kind of a mixture of the two. I created a new class to manage my process instead of a subclass, and I pulled the employees straight from the relationship attribute of the company. Something like this:
#implementation CompanyProcessRunner
-(void)setCompany:(Company *)newCompany
{
company = newCompany;
employees = [company.employees sortedArrayUsingDescriptors:[NSSortDescriptor descriptorWithKey:#"order" ascending:YES]];
}
-(Employee *)nextEmployee
{
company.currentEmployeeIndex += 1;
return (Employee *)[employees objectAtIndex:company.currentEmployeeIndex];
}
#end
Thanks for the help everyone!
To-Many relationships in Core Data are represented by NSSet (as automatically generated by using the Editor... Create NSManagedObject Subclass.. menu.
Which is the most efficient way to iterate an NSSet* ?
NSSet* groups = [contact groups];
for(Group* group in groups) {
NSString* groupName = [group name];
}
or
NSSet* groups2 = [contact groups];
NSArray* groupsArray = [groups2 allObjects];
for(Group* group in groupsArray) {
NSString* groupName = [group name];
}
or another way?
The first is probably more efficient. If the latter were more efficient then Apple would simply use that route to implement the former.
That being said, if you're asking because performance seems to be an issue it's probably more that you're spending a lot of time on the Core Data stuff of faulting objects. A smart move would be to do a fetch on self in %# with groups; set the fetch request's returnsObjectsAsFaults to NO and ensure any appropriate relationshipKeyPathsForPrefetching are specified. The net effect of that will be that all the data you're about to iterate through is fetched in a single trip to the backing store rather than each individual piece of it being fetched on demand in a large number of separate trips.
I have two NSMutableArrays. One array consists of records from the database and the other array consists of records from webservices.
I want to compare each record from the database array to each record in the web services array using a unique key like barcodeID. Also, if the barcodeID key is same then I want to remove the item from the array. It's like I'm updating my database records. If we get the same records from the webservice then I don't want to insert them.
Please help me I'm unable to break the logic for this.
if Product.barcodeID uniquely identifies your objects, then you can use that member to implement -[Product hash] and -[Product isEqual:].
then you can easily use Product in NSSets. NSSet and NSMutableSet contain several methods to combine and remove sets.
The brute force method of doing such comparison is for every record in one array is checked with every record in another. If you find it then stop and discard the object. if you do not find it, then you add it to the array. This of course will have a very high time complexity with a worse case scenario is O(n^2). you could shorten this down by using certain data structures inside your database and web service. Maybe storing them in sorted order or through some algorithm.
You should do some research yourself before asking this question. I shall leave you the option to find a way to optimize your code.
Good luck!
This is kind of the idea of the brute force method. As mentioned above this is incredibly slow compared to alternatives.
- (void)myUpdateFunction
{
NSMutableArray *baseDatabaseArray;
NSMutableArray *baseWebServiceArray;
for (int i = 0; i < baseWebServiceArray.count; i++) {
id value = [[baseWebServiceArray objectAtIndex:i] valueForKey:#"barcodeID"];
NSArray *array = [baseDatabaseArray filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"barcodeID = %#", value]];
if (array.count > 0)
{
id obj = [array objectAtIndex:0];
[baseDatabaseArray removeObject:obj];
}
[baseDatabaseArray addObject:[baseWebServiceArray objectAtIndex:i]];
}
}
I have been using Magical Record and love it.
You have to be using Core Data for this though. Here is what my update code looks like with Magical Record.
- (void)updateDatabase
{
Class class = NSClassFromString(self.managedObjectClassName);
if ([class MR_countOfEntities] > 0) {
for (NSArray *array in self.totalBatches) {
[class MR_updateFromArray:array];
}
} else {
for (NSArray *array in self.totalBatches) {
[class MR_importFromArray:array];
}
}
[self.totalBatches removeAllObjects];
}
If you have any questions about Core Data feel or if you need me to walk through the algorithms feel free to ask.
i need a multilingual coredata db in my iphone app. I could create different database for each language but i hope that in iphone sdk exist an automatically way to manage data in different language core data like for resources and string.
Someone have some hints?
I've done something similar to Shortseller, but without the use of categories.
InternationalBook and LocalizedBook are both custom managed objects with a one-to-many relationship (one international book to many localised books).
In the implementation of InternationalBook, I've added a custom accessor for title:
- (NSString *)title {
[self willAccessValueForKey:#"title"];
NSString *locTitle = nil;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"locale==%#", [DataManager localeString]];
NSSet *localizedSet = [self.localizedBook filteredSetUsingPredicate:predicate];
if ([localizedSet count] > 0) {
locTitle = [[localizedSet valueForKey:#"localizedTitle"] anyObject];
}
[self didAccessValueForKey:#"title"];
return locTitle;
}
[DataManager localeString] is a class method which returns the user's language and country code: en_US, fr_FR, etc. See documentation on NSLocale for details.
See the "Custom Attribute and To-One Relationship Accessor Methods" section of the Core Data Programming Guide for an explanation of willAccessValueForKey: and didAccessValueForKey:.
When populating the data, I grab a string representing the user's current locale ([DataManager localeString]), and store that along the with localised book title in a new LocalizedBook object. Each LocalizedBook instance is added to an NSMutableSet, which represents the one-to-many relationship.
NSMutableSet *bookLocalizations = [internationalBook mutableSetValueForKey:#"localizedBook"]; // internationalBook is an instance of InternationalBook
// set the values for locale and localizedTitle
LocalizedBook *localizedBook = (LocalizedBook *)[NSEntityDescription insertnNewObjectEntityForName:#"LocalizedBook" inManagedObjectContext:self.bookMOC];
localizedBook.locale = [DataManager localeString];
localizedBook.localizedTitle = theLocalizedTitle; // assume theLocalizedTitle has been defined.
[bookLocalizations addObject:localizedBook];
[bookLocalizations setValue:localizedBook forKey:#"localizedBook"];
Since the localised titles are being stored in the LocalizedBook managed object, you can make the title attribute a transient, but if you do that you can't use title in a predicate.
The nice thing about this approach is that the implementation of the to-many relationship is transparent to any consumers. You simply request internationalBook.title and the custom accessor returns the appropriate value based on the user's locale behind the scenes.
I have generated model classes for the core data entities.
Then I defined category helper classes with functions to set and get the multilanguage properties (e.g. name).
So I have a Product (with e.g. code and price) and a ProductLanguage (with language and name properties) Entity.
I never directly access ProductLanguage, but always use the name function defined on the the Product model (via category). This has worked well for me sofar.
Like Gordon, I use quite similar code, but not written files generated by model. I use this code in my .m file where I want to show the data.
As I start from Apple template, I put this code exactly in
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
of my TableViewController.m
P.S: Just to understand, I use these prefixes: tbl_ for tables (entities), rel_ for relationships, fld_ for fields (attributes).
Hope this helps.
NSSet *sourceSet = [NSSet setWithArray:[[tbl_MainTable rel_Localization]allObjects]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"fld_Locale == %#", [[NSLocale preferredLanguages] objectAtIndex:0]];
NSSet *filteredSet = [sourceSet filteredSetUsingPredicate:predicate];
//NSLog(#"%#", filteredSet); NSLog(#"%#", [[filteredSet valueForKey:#"fld_Name"] anyObject]);
if ([filteredSet count] > 0)
{
[cell.detailTextLabel setText:[[filteredSet valueForKey:#"fld_Name"] anyObject]];
}