Skip to content
Robbie Hanson edited this page Dec 16, 2013 · 19 revisions

The Basics

You can store any kind of object you want using YapDatabase!

In order to store an object to disk (via YapDatabase or any other protocol) you need some way of serializing the object. That is, convert the object into a big blob of bytes. And then, to get your object back from the disk you deserialize it (convert big blob of bytes back into object form).

With YapDatabase, you can choose the default serialization/deserialization process, or you can customize it and use your own routines. (In fact, you can have different routines for objects vs metadata.)


The Default Setup

First, let's look at the default serializer & deserializer: NSCoding.

NSCoding is an Apple protocol used to serialize/deserialize an object, and its already supported by a lot of the classes you're already using:

  • NSString
  • NSNumber
  • NSArray
  • NSDictionary
  • NSSet
  • NSData
  • UIColor
  • etc...

So if you want to store objects in the database that already support NSCoding (such as strings, numbers, etc), then you don't have to do a thing. You're good to go.

Otherwise all you have to do is make sure your custom objects implement the 2 NSCoding methods. Before I show you how easy this is, I'll give you an additional link to Apple's thorough documentation.

@interface MyObject : NSObject <NSCoding>
@end

@implementation MyObject
{
    NSString *myString;
    UIColor *myColor;
    MyWhatever *myWhatever;
    float myFloat;
}

- (id)init // Normal init method
{
    if ((self = [super init])) {
        // ...
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)decoder // NSCoding deserialization
{
    if ((self = [super init])) {
        myString = [decoder decodeObjectForKey:@"myString"];
        myColor = [decoder decodeObjectForKey:@"myColor"];
        myWhatever = [decoder decodeObjectForKey:@"myWhatever"];
        myFloat = [decoder decodeFloatForKey:@"myFloat"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)encoder // NSCoding serialization
{
    [encoder encodeObject:myString forKey:@"myString"];
    [encoder encodeObject:myColor forKey:@"myColor"];
    [encoder encodeObject:myWhatever forKey:@"myWhatever"];
    [encoder encodeFloat:myFloat forKey:@"myFloat"];
}

Not exactly rocket science is it? But what about that MyWhatever ivar?

It's simple. If MyWhatever supports NSCoding, then it's initWithCoder/encodeWithCoder method will be called. And everything just works.

It works the same way with arrays. If you have an NSArray of MyObject's, then you can just pass the array to the database. The initWithCoder/encodeWithCoder method of NSArray calls down to the corresponding method of its objects. So you can have arrays (or dictionaries or sets or whatever) that contain your custom objects. And your custom objects can have their own object graph.

Upgrades

One of the hidden benefits of custom serialization/deserialization over Core Data is that you can perform upgrades on the fly. For example, imagine that you need to make changes to an object class in version 2.0 of your app. No problem. You can handle it automatically within initWithCoder, on the fly, as you fetch objects from the database.

- (id)initWithCoder:(NSCoder *)decoder // NSCoding deserialization
{
    if ((self = [super init]))
    {
        int32_t version = [decoder decodeInt32ForKey:@"version"];

        myString = [decoder decodeObjectForKey:@"myString"];
        myColor = [decoder decodeObjectForKey:@"myColor"];
        myWhatever = [decoder decodeObjectForKey:@"myWhatever"];
        myFloat = [decoder decodeFloatForKey:@"myFloat"];

        if (version < 2)
            myName = [self defaultName]; // myName added in v2
        else
            myName = [decoder decodeObjectForKey:@"myName"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)encoder // NSCoding serialization
{
    [coder encodeInt32:2 forKey:@"version"];
    
    [encoder encodeObject:myString forKey:@"myString"];
    [encoder encodeObject:myColor forKey:@"myColor"];
    [encoder encodeObject:myWhatever forKey:@"myWhatever"];
    [encoder encodeFloat:myFloat forKey:@"myFloat"];
    [encoder encodeObject:myName forKey:@"myName"];
}

This is in contrast to Core Data, which is going to incur a performance hit when the user first upgrades, as it adds the proper columns, and then loops over every row in the database to pre-populate the values. Not the hiccup you want when your release notes brag about "performance improvements".


The Details

The init methods allow you to customize exactly how you want to setup serialization & deserialization. From YapDatabase.h:

- (id)initWithPath:(NSString *)path;

- (id)initWithPath:(NSString *)path
        serializer:(YapDatabaseSerializer)serializer
      deserializer:(YapDatabaseDeserializer)deserializer;

- (id)initWithPath:(NSString *)path objectSerializer:(YapDatabaseSerializer)objectSerializer
                                  objectDeserializer:(YapDatabaseDeserializer)objectDeserializer
                                  metadataSerializer:(YapDatabaseSerializer)metadataSerializer
                                metadataDeserializer:(YapDatabaseDeserializer)metadataDeserializer;

As you can see, the serialization & deserialization process is customizable. In fact, you can have different setups for objects vs metadata.

You already know the default serializer/deserializer uses NSCoding. There are other options available out of the box. From YapDatabase.h:

/**
 * The default serializer & deserializer use NSCoding (NSKeyedArchiver & NSKeyedUnarchiver).
 * Thus any objects that support the NSCoding protocol may be used.
 *
 * Many of Apple's primary data types support NSCoding out of the box.
 * It's easy to add NSCoding support to your own custom objects.
**/
+ (YapDatabaseSerializer)defaultSerializer;
+ (YapDatabaseDeserializer)defaultDeserializer;

/**
 * Property lists ONLY support the following: NSData, NSString, NSArray, NSDictionary, NSDate, and NSNumber.
 * Property lists are highly optimized and are used extensively by Apple.
 * 
 * Property lists make a good fit when your existing code already uses them,
 * such as replacing NSUserDefaults with a database.
**/
+ (YapDatabaseSerializer)propertyListSerializer;
+ (YapDatabaseDeserializer)propertyListDeserializer;

/**
 * A FASTER serializer & deserializer than the default, if serializing ONLY a NSDate object.
 * You may want to use timestampSerializer & timestampDeserializer if your metadata is simply an NSDate.
**/
+ (YapDatabaseSerializer)timestampSerializer;
+ (YapDatabaseDeserializer)timestampDeserializer;

And you are free to create your own serialization/deserialization routines.

For YapDatabase:

typedef NSData* (^YapDatabaseSerializer)(NSString *collection, NSString *key, id object);
typedef id (^YapDatabaseDeserializer)(NSString *collection, NSString *key, NSData *data);

So the serializer block takes as parameters the collection, key and object, and returns NSData. And the deserializer block takes as parameters the collection, key and serialized NSData, and returns the object.

So with an easy default serializer/deserializer, and several out-of-the-box alternatives, why create my own?

Here are some examples of why you might want to do so:

  • ENCRYPTION !!! Your custom routine enables on-the-fly encryption/decryption. Now your data-on-disk is secure. And you did it with a few lines of code. And you still get the performance of a database (with built-in object caching too). BOOM.
  • Compression : Storing big long blobs of text? Use zlib compression to deflate/inflate on-the-fly.
  • Performance : If your data has a rigid structure (e.g. you're storing TCP packets), then you may be able to whip up a custom serializer/deserializer routine that performs better and/or takes up less space.
Clone this wiki locally