I'm trying a basic test of sorting an NSManagedObject subclass. I set up a basic subclass "TestClass" with two attributes: stringField and numberField. They use the standard Obj-C 2.0 accessor protocol:
#interface TestClass : NSManagedObject
#property (retain) NSString *stringField;
#property (retain) NSNumber *numberField;
#end
#implementation TestClass
#dynamic stringField;
#dynamic numberField;
#end
When I try to fetch instances of this entity, I can fetch based on either attribute. However, if I use a sort descriptor, the numberField is said to not be KVC-compliant.
Within the model, I set the numberField to Int64, but I'm confused. I thought the wrapper (NSNumber) would handle the KVC problem. What do I need to do to make this work?
Some initial "Is the computer on?"-type questions:
Does your model specify that the managed object class for your entity is TestClass?
Are you sure you spelled numberField correctly when specifying the key in your sort descriptor?
Is numberField a transient attribute in your model?
These are the common issues that I can think of that might cause such an error when fetching with a sort descriptor, the first one especially.
Also, this won't affect KVC, but your attributes' property declarations should be (copy) rather than (retain) since they're "value" classes that conform to the NSCopying protocol and may have mutable subclasses. You don't want to pass a mutable string in and mutate it underneath Core Data. (Yeah, there's no NSMutableNumber or NSMutableDate in Cocoa, but that doesn't prevent creating MyMutableNumber or MyMutableDate subclasses...)
Related
I have a core data database. I'm performing a custom migration. I have a subclass of NSEntityMigrationPolicy. My policy's migration method buildValueFromSectionFieldManagedObject: is being called by a mapping rule : FUNCTION($entityPolicy, "buildValueFromSectionFieldManagedObject:" , $source).
This part is actually working.
However, the implementation of buildValueFromSectionFieldManagedObject: uses methods in the custom entity NSManagedObject subclass of the $source, which is Choice.
The methods of Choice do not seem to be available to the migration function, and instead it gets just a vanilla NSManagedObject.
When I try to use Choice methods, I get an exception. If I po the choice in the debugger, I get something like this:
<NSManagedObject: 0x600000281860> (entity: Choice; id: 0xd00000000038001a ; data: )
Whereas, out of a migration I would usually see something like this:
<Choice: 0x60800028bdb0> (entity: Choice; id: 0x6080002225a0 ; data: {
Is this just how it is, or is there some way that I can use the entity objects during migration?
Possibly relevant – this particular entity, Choice, is removed during this migration. It does not exist in the target managed object model, but does exist in the source managed object model. However, I don't think this is the case as other entity classes that are in the target model are also unavailable as that class during migration – they have class NSManagedObject and their entity methods are not available.
That is correct, you only have access to basic NSManagedObjects during migration.
Three-Stage Migration
The migration process itself is in three stages. It uses a copy of the source and destination models in which the validation rules are disabled and the class of all entities is changed to NSManagedObject.
From: Core Data Model Versioning and Data Migration Guide
Dave's answer clarified that during a migration, core data object are only available as NSManagedObject instances. You don't get to use their Entity classes.
Worse, if you're using a tool like mogenerator, then any handy logic that you've extended the entity classes with is inaccessible.
Poor solutions
Working with an NSManagedObject directly feels dangerous to me. Using [managedObject valueForKey:#"someKey"] is verbose, but worse, there's no compiler checking that you've got your key name correct, so you might be asking for something managedObject doesn't have. There's also no compiler checking of the returned type either – it could be anything that you can put in to a managed object.
Slightly better is [managedObject valueForKey: NSStringFromSelector(#selector(someKey))] is safer, but horribly verbose and awkward, and still isn't that safe – lots of things might implement the method someKey.
You might also declare your keys as literals:
NSString *const someKey = #"someKey";
[managedObject valueForKey: someKey];
Again – this is slightly safer, less error prone and verbose, but you've got no guarantee still that this managed object has someKey.
None of these approaches will give us access to custom logic, either.
Better solution
What I did instead of this was to define a protocol for the properties that I know my managed object has. Here's are examples for entities Choice and ChoiceType.
#protocol ChoiceProtocol
- (NSSet<id <ChoiceTypeProtocol> > *)choiceTypes;
- (NSNumber *)selected;
- (NSNumber *)order;
#end
#protocol ChoiceTypeProtocol
- (NSNumber *)selected;
- (NSString *)name;
- (NSString *)textCustom;
- (NSNumber *)order;
#end
Now in my migration code, instead of having a custom migration function similar to:
- (NSString *)migratedRepresentationOfChoice:(NSManagedObject *)choice;
I have:
- (NSString *)migratedRepresentationOfChoice:(id <ChoiceProtocol>)choice;
In the body of this function I can use choice exactly as I would any regular object. I get code completion as I type, I get the right syntax highlighting, the compiler will complain if I call a non existent method.
I also get return type checking, so the compiler will complain if I use the NSNumber property selected as an NSString or NSSet. And it'll also be helpful and suggest NSNumber methods as completions while I type.
How about the logic?
This approach doesn't provide the logic you've added to entity classes.
In Swift you might be able to use a protocol extension to achieve that.
I was retiring these entities (hence the migration), so I moved the entity logic functions I needed in to a helper functions of my custom migration. As an example choice.orderedChoiceTypes becomes [self choiceOrderedChoiceTypes:choice].
In future I will probably avoid ever adding logic to NSManagedObject entities. I think it's probably a better plan to put any such logic in domain objects that you build from your managed objects. Further, I will probably avoid defining entity classes and instead only access NSManagedObject instances through a protocol as during the migration. It seems clean, simple, removes magic, and has benefits for testing – not just for migrations.
I'm using custom classes for my CoreData stack. Class properties are set properly in the model. There's some points in the app that are abstracted to use setValue... on an NSManagedObject, but I have a few cases where that fails with an NSInvalidArgumentException, specifically when setting a related object; error indicates it wants the specific type, and it's' getting an NSManagedObject, hence the error. So, I thought I would just take the short route and cast my instance prior to the offending call, if it's of a certain entity; like this:
NSManagedObject *addressObject = [NSEntityDescription insertNewObjectForEntityForName:#"Address" inManagedObjectContext:[object managedObjectContext]];
if ([[[object entity]name] isEqualToString:#"Hospital"]) {
Contact *contact = (Contact*)object;
DLog(#"The class of contact is: %#", [contact class]);
contact.Address = addressObject;
}
else{
[object setValue:addressObject forKey:#"Address"];
}
I know, Address shouldn't be capitalized; I inherited this mess... anyway, I would totally expect that the contact object is a Contact, but it's not, it's an NSManagedObject! What am I doing wrong with the cast? Everything I've uncovered says this is the right way to cast, but for some reason, it's not working for me here. Of course, this wouldn't be necessary if the addressObject didn't complain about getting an NSManagedObject instead of a Contact (sorry, Hospital inherits from Contact here), and that's another baffling thing, but first things first. How can I coerce object to type Contact, which it really is?
Here's the relevant trace:
* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unacceptable type of value for to-one relationship: property = "Contact"; desired type = Contact; given type = NSManagedObject; value = ...
For completeness sake, the Address class has declaration for Contact as:
#property (nonatomic, retain) NSManagedObject * Contact;
with implementation for Contact as the normal dynamic, like:
#dynamic Contact;
Maybe I need some sleep? ;-) Thanks
The cast isn't your problem. Casting doesn't change what class an object is — just what the compiler thinks it is.
You object is an NSManagedObject but not a Contact instance. In you code the object you have is a Hospital entity. Double-check that the Hospital entity is set to use the Contact class (or a subclass)).
It is important to note that Entity inheritance and Objective-C class inheritance don't have to match. I.e. you can have Hospital be a subentry of Contact and still have the Contact entity map to a Contact class without Hospital map to a subclass of the Contact class. It is valid for the Hospital entity to map to the (same) Contact class or even to NSManagedObject (which is what I suspect you've done).
This may seem confusing but can be very powerful if used correctly.
So yeah, turns out I did need some sleep;-) The problem was the subclass not being included in the target. I know I've experienced similar issues when you have a subclass, and it's in the target, but you forget to define the custom subclass in the model, too.
NSManagedObject provides access to its NSManagedObjectContext, but does it retain it?
According to "Passing Around a NSManagedObjectContext on iOS" by Marcus Zarra, "The NSManagedObject retains a reference to its NSManagedObjectContext internally and we can access it."
How does Zarra know this and is he correct?
I'm asking because I want to know if the NSManagedObjectContext will be dealloc'ed in the tearDown method below. (I'm using CocoaPlant.)
#import <SenTestingKit/SenTestingKit.h>
#import <CocoaPlant/CocoaPlant.h>
#import "AccountUser.h"
#interface AccountUserTests : SenTestCase {
AccountUser *accountUser;
}
#end
#implementation AccountUserTests
- (void)setUp {
accountUser = [AccountUser insertIntoManagedObjectContext:
[NSManagedObjectContext contextWithStoreType:NSInMemoryStoreType error:NULL]];
}
- (void)tearDown {
[accountUser delete];
}
- (void)testFetchWithLinkedAccountUserID {
// Tests go here...
}
#end
NSManagedObject DOES NOT hold strong reference to its NSManagedObjectContext. I've checked that on a test project.
Therefore, you should keep strong reference to NSManagedObjectContext as long as you use its objects.
Wow, over two years old and no accepted answer here :)
When I wrote that post I did indeed mean it keeps a reference to its associated NSManagedObjectContext. If a NSManagedObject retained the NSManagedObjectContext then it would most likely run into problems.
In either case, whether the MOC is retained by the MO is irrelevant to your application design. If you need the MOC to stay around then you need to retain it (now referred to as a strong reference) or it will go away. What the frameworks do internally is not our problem. We just need to make sure we balance our retains and releases.
Matt,
I think Marcus may have miswrote that a NSManagedObject retains its context. Every NSManagedObject maintains a link to the context. Unless individual objects have an internal retain cycle or are retained outside of their context, then, in my experience, they are all released when the context is released. If they retained the context, then this would almost certainly not be the case.
The above said, you can easily write code to test Marcus' claim. Override -dealloc and log when it is called.
IMO, it is a best practice to retain your context until you are done with it. Depending on an undocumented behavior is probably not wise.
Andrew
-setPrimitiveValue:forKey: won't trigger KVO notifications. But in my brain, KVO only makes sense when something changes. But how can change something when I only access it for read?
-primitiveValueForKey: only gets the object for some key. But it won't modify it. So why would/could this cause KVO notifications when using -valueForKey:?
(Of course there is a point, but I don't see it yet.)
The -primitiveValueForKey: and -setPrimitiveValue:forKey: methods are primarily used by Core Data. In particular, Core Data doesn't just need to know when you're going to modify an attribute; it needs to know when you're going to access it as well, so it can implement faulting.
Thus Core Data adds -{will,did}AccessValueForKey: methods to use in getters, just as -{will,did}ChangeValueForKey: methods exist for use in setters to act as KVO hooks.
However, there's another wrinkle: Core Data actually manages the underlying storage of modeled properties for you as well. So you need some way to manipulate this underlying storage within the barriers established by the -{will,did}{Access,Change}ValueForKey: methods. That's where -primitiveValueForKey: and -setPrimitiveValue:forKey: come in.
This is why the standard pattern for implementing Core Data getters and setters, prior to the existence of #property and #dynamic, looked like this:
// Person.m
#import "Person.h"
#implementation Person
- (NSString *)name {
[self willAccessValueForKey:#"name"];
NSString *value = [self primitiveValueForKey:#"name"];
[self didAccessValueForKey:#"name"];
}
- (void)setName:(NSString *)value {
[self willChangeValueForKey:#"name"];
[self setPrimitiveValue:value forKey:#"name"];
[self didChangeValueForKey:#"name"];
}
#end
Now of course, you can just declare a property and define it as #dynamic if you want Core Data to generate this stuff for you at runtime:
// Person.h
#interface Person : NSManagedObject
#property (nonatomic, readwrite, copy) NSString *name;
#end
// Person.m
#import "Person.h"
#implementation Person
#dynamic name;
#end
There are some situations though where you still want to manipulate the underlying storage without KVO or fault-firing, however. Thus Core Data provides a new way to get at this as well, also built around property declarations and automatic synthesis:
// Person.h
#interface Person : NSManagedObject
#property (nonatomic, readwrite, copy) NSString *name;
#end
// Person.m
#import "Person.h"
#interface Person ()
#property (nonatomic, readwrite, copy) NSString *primitiveName;
#end
#implementation Person
#dynamic name;
#dynamic primitiveName;
#end
Note that I put the class continuation in the .m file; it's not something that code outside Person (or even really code outside -awakeFromInsert and -awakeFromFetch) should touch. But it does let me get at the underlying storage for the "name" property without embedding literal strings in my code, and with the use of the real types.
I have several subclasses of NSManagedObject. They are all instantiated with code something like this:
MeasurementDescriptor *descriptor = (MeasurementDescriptor *)[NSEntityDescription
insertNewObjectForEntityForName:#"MeasurementDescriptor"
inManagedObjectContext:context];
or like this:
Experiment *experiment = (Experiment *)[NSEntityDescription
insertNewObjectForEntityForName:#"Experiment"
inManagedObjectContext:context];
What is odd, though, is that (from code above)
NSLog(#" descriptor's class = %#", NSStringFromClass([descriptor class]));
prints out 'NSManagedObject', while
NSLog(#" experiment's class = %#", NSStringFromClass([experiment class]));
prints out 'Experiment'.
Does anyone know why this would be? MeasurementDescriptor, unlike my other NSManagedObject subclasses, had no ivars (not including its Core Data properties). Adding an ivar did not change anything. Similarly, MeasurementDescriptor is the only NSManagedObject subclass without 'relationship' properties. Perhaps this accounts for this strangeness...???
The only explaination is that your MeasurementDescriptor subclass is not actually known to the code. The most common causes of this are:
In the data model editor not setting the Class attribute of the entity to the correct class.
Not adding the source file for the subclass to the target.
This is easy to do with Core Data because if it can't find a dedicated subclass it doesn't complain but just returns a generic NSManagedObject initialized with the entity's property key names.