-
Notifications
You must be signed in to change notification settings - Fork 365
MyDatabaseObject
Tips and tricks for making YapDatabase even easier.
YapDatabase itself is object agnostic. That is, unlike many other databases, it doesn't force you to use a base class. The object policy is simply: if you can serialize/deserialize it, then YapDatabase can use it.
More information about this can be found in the Storing Objects wiki page. In fact, it even mentions several 3rd party frameworks that you might wish to use as your base class:
Ultimately, you decide what the best object setup is for your app. And YapDatabase can accommodate you.
What follows are some tips & tricks (related to model objects) that we've accumulated over the years. We've combined them all together, and placed them into a sample project that's available within the repository.
As we review the neat little tricks this class contains, keep in mind that you're welcome to copy any of the code from this class. You can copy the whole thing, and rename it to match your project. Or just copy the bits & pieces you want. (Hey, maybe you'd like to use some of this stuff for non-YapDatabase purposes. That's OK too!)
Immutable classes are great! You can pass them around between threads, and never worry about stepping on toes. This is why Apple's Foundation classes often have both immutable & mutable variants. (NSString & NSMutableString, NSArray & NSMutableArray, NSDictionary & NSMutableDictionary ...) Being able to take an object, and make an immutable copy is incredibly useful. (P.S. calling copy
on an immutable object generally returns self
. So it's no more expensive than a retain
.)
However, how often do you make both immutable & mutable variants for your own classes ?!? The answer for most people is never. And why not? Probably because it would be a big pain in the rear! Not only that, but it could quickly complicate your ability to subclass your model objects. (Think about subclassing MyCar when MyMutableCar is already a subclass...)
Luckily we can use KVO tricks to enforce immutability when we want it. Here's the API:
@property (nonatomic, readonly) BOOL isImmutable;
- (void)makeImmutable;
- (instancetype)mutableCopy;
- (instancetype)immutableCopy;
- (NSException *)immutableExceptionForKey:(NSString *)key;
And here's how easy it is to use:
#import "MyDatabaseObject.h"
@interface Car : MyDatabaseObject
@property (nonatomic, copy, readwrite) NSString *make;
@property (nonatomic, copy, readwrite) NSString *model;
@end
@implementation Car
@synthesize make;
@synthesize model;
- (id)copyWithZone:(NSZone *)zone
{
Car *copy = [super copyWithZone:zone];
copy->make = make;
copy->model = model;
return copy;
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
Car *car = [[Car alloc] init];
car.make = @"Tesla";
car.model = @"Model S";
[car makeImmutable];
car.make = @"Ford"; // Throws exception: Attempting to mutate immutable object...
Car *car2 = [car mutableCopy];
car2.model = @"Model X"; // No problem
}
If you're wondering how this works, you're welcome to look at the code in MyDatabaseObject.m. It's actually pretty easy to do using standard KVO techniques.
The only things to look out for are:
- if you have custom setters, then you'll need to properly perform KVO calls
- mutable ivars, which are strongly discouraged
Let's take a look at these problems, and how we can solve them.
#import "MyDatabaseObject.h"
@interface Car : MyDatabaseObject
@property (nonatomic, copy, readwrite) NSString *make;
@property (nonatomic, copy, readwrite) NSString *model;
@property (nonatomic, assign, readwrite) NSUInteger speed;
@property (nonatomic, strong, readwrite) NSMutableArray *passengers; // Naughty programmer !
@end
@implementation Car
@synthesize make;
@synthesize model;
@synthesize speed;
@synthesize passengers;
- (void)setSpeed:(NSInteger)newSpeed
{
speed = MIN(newSpeed, MAX_SPEED); // Oops, missing KVO stuff !
}
@end
Luckily the solutions are very straight-forward:
- Use immutable properties whenever possible. It's good defensive programming anyway, especially in a concurrent system.
- Add KVO method calls of
willChangeValueForKey:
&didChangeValueForKey:
#import "MyDatabaseObject.h"
@interface Car : MyDatabaseObject
@property (nonatomic, copy, readwrite) NSString *make;
@property (nonatomic, copy, readwrite) NSString *model;
@property (nonatomic, assign, readwrite) NSUInteger speed;
@property (nonatomic, strong, readwrite) NSArray *passengers; // make immutable
- (void)addPassenger:(NSString *)newPassenger; // add convenience methods instead
@end
@implementation Car
@synthesize make;
@synthesize model;
@synthesize speed;
@synthesize passengers;
- (void)setSpeed:(NSInteger)newSpeed
{
[self willChangeValueForKey:@"speed"];
speed = MIN(newSpeed, MAX_SPEED);
[self didChangeValueForKey:@"speed"];
}
- (void)addPassenger:(NSString *)newPassenger
{
if (newPassenger == nil) return;
[self willChangeValueForKey:@"passengers"];
if (passengers)
passengers = [passengers arrayByAddingObject:newPassenger];
else
passengers = @[ newPassenger ];
[self didChangeValueForKey:@"passengers"];
}
@end
The class is configurable too. For example, you may have properties which are simply used for caching:
@property (nonatomic, strong, readwrite) UIImage *avatarImage;
@property (nonatomic, strong, readwrite) UIImage *cachedTransformedAvatarImage;
In this example, you store the user's plain avatar image. However, your code transforms the avatar in various ways for display in the UI. So to reduce overhead, you'd like to cache these transformed images in the user object. Thus the cachedTransformedAvatarImage
property doesn't actually mutate the user object. It's just a temp cache.
No problem. In your subclass you just override a single method like so:
+ (NSMutableSet *)monitoredProperties
{
NSMutableSet *monitoredProperties = [super monitoredProperties];
[monitoredProperties removeObject:@"cachedTransformedAvatarImage"];
return monitoredProperties;
}
The real beauty of this technique comes when you combine it with YapDatabase. Because you can use various hooks within YapDatabase to ensure:
- every object you put into YapDatabase automatically gets made immutable
- every object you read from YapDatabase is always returned immutable
// We just slightly tweak the default serializer
// in order to ensure that objects coming out of the database are immutable.
YapDatabaseDeserializer deserializer = ^(NSString *collection, NSString *key, NSData *data){
id object = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if ([object isKindOfClass:[MyDatabaseObject class]])
{
[(MyDatabaseObject *)object makeImmutable];
}
return object;
};
// And we make sure that all objects going into the database get made immutable.
YapDatabasePreSanitizer preSanitizer = ^(NSString *collection, NSString *key, id object){
if ([object isKindOfClass:[MyDatabaseObject class]])
{
[object makeImmutable];
}
return object;
};
database = [[YapDatabase alloc] initWithPath:databasePath
serializer:nil
deserializer:deserializer
preSanitizer:preSanitizer
postSanitizer:nil
options:nil];
But wait, there's more !!!
If you use this technique for every object you put into the database, then you can take advantage of a neat performance optimization. This is described in more detail in the Object Policy wiki page. But the summary is this: You can tell YapDatabase that every object is immutable, and therefore share-able between threads/connections. And it will automatically optimize how it goes about updating internal caches. Which can result in a significant decrease in disk IO !
If we can monitor an object to enforce immutability, can we track which properties have been modified?
Yup! We sure can!
@property (nonatomic, readonly) NSSet *changedProperties;
@property (nonatomic, readonly) BOOL hasChangedProperties;
- (void)clearChangedProperties;
And again, this gets even sweeter when you combine it with YapDatabase. Because you can configure YapDatabase to automatically invoke clearChangedProperties
at the appropriate time.
// We just slightly tweak the default serializer
// in order to ensure that objects coming out of the database are immutable.
YapDatabaseDeserializer deserializer = ^(NSString *collection, NSString *key, NSData *data){
id object = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if ([object isKindOfClass:[MyDatabaseObject class]])
{
[(MyDatabaseObject *)object makeImmutable];
}
return object;
};
// We make sure that all objects going into the database get made immutable.
YapDatabasePreSanitizer preSanitizer = ^(NSString *collection, NSString *key, id object){
if ([object isKindOfClass:[MyDatabaseObject class]])
{
[object makeImmutable];
}
return object;
};
// We make sure to clear changedProperties after the item has gone through the database & extensions
YapDatabasePostSanitizer postSanitizer = ^(NSString *collection, NSString *key, id object){
if ([object isKindOfClass:[MyDatabaseObject class]])
{
[object clearChangedProperties];
}
};
database = [[YapDatabase alloc] initWithPath:databasePath
serializer:nil
deserializer:deserializer
preSanitizer:preSanitizer
postSanitizer:postSanitizer
options:nil];
You'll notice there are separate pre & post sanitizers. Here's how it works:
- The
preSanitizer
is invoked immediately at the beginning ofsetObject:forKey:inCollection:
. So it's able to modify the object before it gets serialized, and before its passed to all the registered extensions. - The
postSanitizer
is invoked at the very end ofsetObject:forKey:inCollection:
. So this is AFTER the object has been passed to all the registered extensions.
In other words, your extension will be able to query the object to see exactly which properties have been changed. And this can be very handy. In fact, the YapDatabaseCloudKit uses this technique to optimize exactly which properties it chooses to send to the cloud!