Core Data adapter #87

Merged
merged 27 commits into from May 21, 2013

Projects

None yet

10 participants

Owner

A MTLManagedObjectAdapter, for transforming MTLModel objects to and from NSManagedObjects.

To do:

  • Serialize collections to NSManagedObject relationships
  • Deserialize relationships to MTLModel collections
  • Unit tests for serialization
  • Unit tests for deserialization
  • Check +classForDeserializingManagedObject: before deserialization
  • Track objects serialized/deserialized so far to avoid cycles
  • Document private MTLManagedObjectAdapter methods
Owner

I'm especially interested for @alanjrogers and @dannygreg to weigh in on this one, as Core Data users.

Contributor

The interface looks good. I'm curious how you were planning on handling faults?

👍 on the API.

I'm assuming all fetching etc. would be carried out at the CoreData level and then transformed into Mantle model objects to bubble up to the UI etc.?

Owner

I'm curious how you were planning on handling faults?

What do you mean? The only time faulting would occur from this API is when deserializing an NSManagedObject into MTLModel. The implementation should be thread-safe, so any faulting can happen in the background if you're careful with the contexts you use.

I'm assuming all fetching etc. would be carried out at the CoreData level and then transformed into Mantle model objects to bubble up to the UI etc.?

I see two major use cases here:

  1. Hit a web service, parse its JSON into MTLModels, then trivially save those into Core Data.
  2. Read objects out of Core Data as MTLModels, then use those in your app (exactly what you described). This nicely hides a lot of the complexity of Core Data. It could result in worse performance, but it's an easy way to persist objects sensibly. Reading objects like this would be similar to an NSDictionaryResultType fetch, but more powerful.

They go hand-in-hand conceptually, but you could also easily do one without the other. For example, maybe you just use Mantle as an intermediary for JSON parsing, but don't actually reference MTLModel anywhere outside your model layer.

You could also just use Core Data directly if performance becomes a serious concern.

Contributor

What do you mean? The only time faulting would occur from this API is when deserializing an NSManagedObject > into MTLModel. The implementation should be thread-safe, so any faulting can happen in the background if you're > careful with the contexts you use.

Actually now that I've thought about it some more, any MOs would have their faults fire when you serialized them into MTL objects, so there is nothing we need to worry about.

jspahrsummers added some commits Apr 1, 2013
@jspahrsummers jspahrsummers Stub methods 4eed118
@jspahrsummers jspahrsummers Move MTLManagedObjectAdapter into the main project
As long as we don't ever reference Core Data classes directly, we should be able to use methods from them.
0d0faa1
@jspahrsummers jspahrsummers Implement basic MTLModel > NSManagedObject serialization 0db55fa
@jspahrsummers jspahrsummers Implement basic NSManagedObject > MTLModel serialization 694d184
@jspahrsummers jspahrsummers Skip validation for relationships, serialize to-many relationships ec8370e
@jspahrsummers jspahrsummers Note that MTLModel to-many relationships only support arrays and sets f7d6919
@jspahrsummers jspahrsummers Implement deserialization of to-many relationships d473f05
@jspahrsummers jspahrsummers Actually invoke +classForDeserializingManagedObject: 29b17f9
@jspahrsummers jspahrsummers Set up some test models for Core Data f266881
@jspahrsummers jspahrsummers Merge branch 'master' into core-data-adapter 271de9f
@jspahrsummers jspahrsummers Tests for NSManagedObject > MTLModel b2161f1
@jspahrsummers jspahrsummers Avoid -performBlockAndWait: for confined contexts 0222b4e
@jspahrsummers jspahrsummers Tests for MTLModel > NSManagedObject 60eb8f9
@jspahrsummers jspahrsummers Add a transformer for numberString 9dadf54
@jspahrsummers jspahrsummers Track models created so far to avoid cycles d9d75a3
@jspahrsummers jspahrsummers Add a property mapping for requiredString efea117
@jspahrsummers jspahrsummers Fix saving error 2b91704
@jspahrsummers jspahrsummers Track managed objects created so far, fix recursion 20ad87b
@jspahrsummers jspahrsummers Ask models for entity names, not raw entity descriptions
Otherwise weird Core Data stack conflicts occur when setting
relationships.
b47adeb
@jspahrsummers jspahrsummers Document private MTLManagedObjectAdapter methods 658099e
Owner

@github/mac @mdiep @joshvera This is ready for review now. 💥

CC @mattt

How would you combine the power of MTLJSONAdapter to parse incoming JSON dictionaries and MTLManagedObjectAdapter to create NSManagedObject instances?

My first bet is using + (NSManagedObject *)managedObjectFromModel:insertingIntoContext:error:, but I don't know if there is a better alternative.

Owner

@elitalon Yes, that's exactly how.

@jspahrsummers Thanks! Great job here 👍

@alanjrogers alanjrogers and 1 other commented on an outdated diff May 20, 2013
Mantle/MTLManagedObjectAdapter.h
+// Specifies the MTLModel subclasses that should be deserialized to the
+// receiver's property keys when a property key corresponds to an entity
+// relationship.
+//
+// In other words, the dictionary returned by this method is used to decode
+// managed object relationships into MTLModels (or NSArrays or NSSets thereof)
+// set on the receiver.
+//
+// If a property key is omitted from the returned dictionary, but present in
+// +managedObjectKeysByPropertyKey, and the receiver's +managedObjectEntity has
+// a relationship at the corresponding key, an exception will be thrown during
+// deserialization.
+//
+// Subclasses overriding this method should combine their values with those of
+// `super`.
++ (NSDictionary *)relationshipModelClassesByPropertyKey;
alanjrogers
alanjrogers May 20, 2013 Contributor

This could do with a "Returns a dictionary mapping property keys (as an NSString) to model classes (as Class)." statement as the last paragraph. While the rest of the documentation already explains this and then method name also spells it out, I still find the 'Returns blah' bit of the docs to be a very useful quick reference.

@alanjrogers alanjrogers commented on the diff May 20, 2013
Mantle/MTLManagedObjectAdapter.m
@@ -0,0 +1,527 @@
+//
+// MTLManagedObjectAdapter.m
+// Mantle
+//
+// Created by Justin Spahr-Summers on 2013-03-29.
+// Copyright (c) 2013 GitHub. All rights reserved.
+//
+
+#import "MTLManagedObjectAdapter.h"
+#import "EXTScope.h"
+#import "MTLModel.h"
+#import "MTLReflection.h"
+
+NSString * const MTLManagedObjectAdapterErrorDomain = @"MTLManagedObjectAdapterErrorDomain";
alanjrogers
alanjrogers May 20, 2013 Contributor

Could use NSSTRING_CONST here. Do we have it defined in Mantle?

jspahrsummers
jspahrsummers May 20, 2013 Owner

/nope

I'm not the biggest fan of that macro, at least for our OSS. It doesn't save that much typing, and its purpose is not super intuitive when seeing it.

alanjrogers
alanjrogers May 20, 2013 Contributor

TBH Neither am I, I'd be fine with getting rid of it :trollface:

@alanjrogers alanjrogers commented on the diff May 20, 2013
Mantle/MTLManagedObjectAdapter.m
+
+NSString * const MTLManagedObjectAdapterErrorDomain = @"MTLManagedObjectAdapterErrorDomain";
+const NSInteger MTLManagedObjectAdapterErrorNoClassFound = 2;
+const NSInteger MTLManagedObjectAdapterErrorInitializationFailed = 3;
+const NSInteger MTLManagedObjectAdapterErrorInvalidManagedObjectKey = 4;
+const NSInteger MTLManagedObjectAdapterErrorUnsupportedManagedObjectPropertyType = 5;
+const NSInteger MTLManagedObjectAdapterErrorUnsupportedRelationshipClass = 6;
+
+// Performs the given block in the context's queue, if it has one.
+static id performInContext(NSManagedObjectContext *context, id (^block)(void)) {
+ if (context.concurrencyType == NSConfinementConcurrencyType) {
+ return block();
+ }
+
+ __block id result = nil;
+ [context performBlockAndWait:^{
alanjrogers
alanjrogers May 20, 2013 Contributor

This will deadlock if called on the main thread and the context has a concurrencyType of NSMainQueueConcurrencyType.

jspahrsummers
jspahrsummers May 20, 2013 Owner

It's poorly documented, but performBlockAndWait: is specifically meant to support that kind of recursive locking. I'll add a test verifying it.

alanjrogers
alanjrogers May 20, 2013 Contributor

Oh I didn't know that. Do you have link for the docs?

jspahrsummers
jspahrsummers May 20, 2013 Owner

This was ridiculously hard to track down. It's only mentioned in the high-level iOS 5 Release Notes (not OS X), and no specific Core Data document. Specifically:

The performBlockAndWait: method supports API reentrancy.

alanjrogers
alanjrogers May 21, 2013 Contributor

👍 WOW Awesome documentation there Apple.

@alanjrogers alanjrogers commented on the diff May 20, 2013
Mantle/MTLManagedObjectAdapter.m
+// The MTLModel subclass being serialized or deserialized.
+@property (nonatomic, strong, readonly) Class modelClass;
+
+// A cached copy of the return value of +managedObjectKeysByPropertyKey.
+@property (nonatomic, copy, readonly) NSDictionary *managedObjectKeysByPropertyKey;
+
+// A cached copy of the return value of +relationshipModelClassesByPropertyKey.
+@property (nonatomic, copy, readonly) NSDictionary *relationshipModelClassesByPropertyKey;
+
+// Initializes the receiver to serialize or deserialize a MTLModel of the given
+// class.
+- (id)initWithModelClass:(Class)modelClass;
+
+// Invoked from +modelOfClass:fromManagedObject:processedObjects:error: after
+// the receiver's properties have been initialized.
+- (id)modelFromManagedObject:(NSManagedObject *)managedObject processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error;
alanjrogers
alanjrogers May 20, 2013 Contributor

I'm curious, Why the CFMutableDictionaryRef over NSMutableDictionary?

jspahrsummers
jspahrsummers May 20, 2013 Owner

Need to use pointer equality instead of -isEqual: (which is unnecessarily expensive, and might create false positives). It's documented in a comment in the implementation below.

alanjrogers
alanjrogers May 20, 2013 Contributor

I can't find that comment, could you link it?

@alanjrogers alanjrogers commented on the diff May 20, 2013
Mantle/MTLManagedObjectAdapter.m
+ // any cycles when processing its relationships.
+ CFDictionaryAddValue(processedObjects, (__bridge void *)model, (__bridge void *)managedObject);
+
+ NSDictionary *dictionaryValue = model.dictionaryValue;
+ NSDictionary *managedObjectProperties = managedObject.entity.propertiesByName;
+
+ [dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *propertyKey, id value, BOOL *stop) {
+ NSString *managedObjectKey = [self managedObjectKeyForKey:propertyKey];
+ if (managedObjectKey == nil) return;
+ if ([value isEqual:NSNull.null]) value = nil;
+
+ BOOL (^serializeAttribute)(NSAttributeDescription *) = ^(NSAttributeDescription *attributeDescription) {
+ // Mark this as being autoreleased, because validateValue may return
+ // a new object to be stored in this variable (and we don't want ARC to
+ // double-free or leak the old or new values).
+ __autoreleasing id transformedValue = value;
alanjrogers
alanjrogers May 20, 2013 Contributor

May be worth putting this comment in the setValueForKey block inside modelFromManagedObject... too.

@alanjrogers alanjrogers was assigned May 20, 2013
Contributor

I've noticed that the implementation of modelFromManagedObject and managedObjectFromModel are very similar, with the only apparent difference being the direction of serialization. Do you think it would be possible to factor out the common stuff into another method?

Contributor

📎

Owner

Do you think it would be possible to factor out the common stuff into another method?

I don't think so. The direction has a pretty big impact, because the logic for deserializing is very different from that of serializing (in terms of methods invoked, objects created, etc.), even though the general structure is similar.

Owner

📋

@joshvera joshvera commented on the diff May 20, 2013
Mantle/MTLManagedObjectAdapter.m
+ if (error != NULL) {
+ NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"No property by name \"%@\" exists on the entity.", @""), managedObjectKey];
+
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Could not deserialize managed object", @""),
+ NSLocalizedFailureReasonErrorKey: failureReason,
+ };
+
+ *error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorInvalidManagedObjectKey userInfo:userInfo];
+ }
+
+ return NO;
+ }
+
+ // Jump through some hoops to avoid referencing classes directly.
+ NSString *propertyClassName = NSStringFromClass(propertyDescription.class);
joshvera
joshvera May 20, 2013 Owner

I don't understand why we're doing this. We don't want the block to retain these classes during execution?

joshvera
joshvera May 20, 2013 Owner

Oh never mind. Missed the conditional linking.

jspahrsummers
jspahrsummers May 20, 2013 Owner

Yeah, the goal is that Mantle shouldn't require Core Data to be linked unless you're using this adapter.

Contributor

📚

@alanjrogers alanjrogers merged commit b6dc288 into master May 21, 2013

1 check passed

default Build #410187 succeeded in 28s
Details
@alanjrogers alanjrogers deleted the core-data-adapter branch May 21, 2013
Owner

🤘

bachand commented Jul 19, 2013

@jspahrsummers It's very possible I'm missing something, but I couldn't find a way to easily update an existing NSManagedObject using MTLManagedObjectAdapter. Is there any facility for that workflow?

Owner

@bachand There's currently no way to do that, but I can see how it'd be useful when going from JSON > Mantle > Core Data. I'd be happy to review a pull request.

bachand commented Jul 19, 2013

@jspahrsummers Thanks for the quick response! I'll let you know if I decide to go down that route.

Owner
robb commented Sep 30, 2013

@bachand did you ever go down that route?

bachand commented Oct 1, 2013

@robb I was able to get https://github.com/RestKit/RestKit to fit my needs, so I didn't end up going down that route. What may not be totally apparent about RestKit at first glance is that it is very easy to only use a handful of its classes without adopting its whole request/response mapping framework.

alvaromb commented Mar 3, 2014

@bachand AFAIK, what do you want to achieve can be done with propertyKeysForManagedObjectUniquing. Here you can find what this method does:

// The managed object returned by the fetch request will then be set with all
// values from the MTLModel that the managed object is being converted from.
bachand commented Mar 14, 2014

@alvaromb, thanks for pointing that out – you're absolutely right. Unfortunately as far as I can tell this wasn't released when I was looking into using Mantle, but great to know moving forward!

It would be great if we just convert from Model to ManagedObject, WITHOUT saving to context. What if we want to use MagicalRecord do the context saving part

Member

@onmyway133 But there's no saving. MTLManagedObjectAdapter just creates or updates a managed object, that's it.

@onmyway133 Overcoat 2.0 uses Mantle and AFNetworking, and provides automatic context save for your responses, because it uses a private queue to serialise from JSON to MTLModel and then to NSManagedObject. And, as @nickynick just said, there is no saving using Mantle's MTLManagedObjectAdapter, it just generates the NSManagedObject's.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment