-
Notifications
You must be signed in to change notification settings - Fork 365
Relationships
There's an extension for that.
The YapDatabaseRelationship extension allows you to create a "relationship" between any two objects in the database. If you're familiar with relationships in Core Data, it works very much the same way.
- Graph concepts
- Example 1: Parent -> child relationship (one-to-one)
- Example 2: Parent -> children relationships (one-to-many)
- Example 3: Parent -> child relationship + Notify
- Example 4: Reference counting relationship
- Edge creation
- Querying for edges
- Differences with Core Data
- Graph Processing
In common graph parlance, each object is called a node, and the line between the two nodes is called an edge.
(nodeA) -----> (nodeB) edge
YapDatabaseRelationship allows you to specify the edges, along with a rule to specify what should happen if one of the 2 nodes gets deleted from the database.
The edges are directional, however the associated delete rules are bidirectional. Thus you can create the edge in either direction, and setup the delete rules to match whatever you need.
In terms of edge direction, the edge starts from the "source" node. (nodeA in the example above) And the object at the other end of the edge is called the "destination" node. (nodeB in the example above)
Every edge has a name. This is simply a string that you define, which can be anything you want (except nil). The name is useful when searching the graph for particular edges. And it also comes into play when you create one-to-many, or many-to-many relationships.
Every edge also has a property called the 'nodeDeleteRules'. This is a bitmask, which you can specify using the following constants:
enum {
// notify only
YDB_NotifyIfSourceDeleted = 1 << 0,
YDB_NotifyIfDestinationDeleted = 1 << 1,
// one-to-one
YDB_DeleteSourceIfDestinationDeleted = 1 << 2,
YDB_DeleteDestinationIfSourceDeleted = 1 << 3,
// one-to-many & many-to-many
YDB_DeleteSourceIfAllDestinationsDeleted = 1 << 4,
YDB_DeleteDestinationIfAllSourcesDeleted = 1 << 5,
};
typedef int YDB_NodeDeleteRules;
Note that because it is a bitmask, you can combine multiple rules together. But not all rule combinations are legal (because they don't make logical sense, as you'll see shortly).
Let's look at a few examples:
### Example 1: Parent -> child relationship (one-to-one)In this situation, the "parent" owns the "child", and the child object should be removed from the database if the parent is removed from the database. Usually the child object is a part of the parent, and only makes sense if the parent exists. For example, the parent object is a "Player", and the child object is an "Avatar".
So you could create the edge like this:
(player) ----------> (avatar) @"avatar",[YDB_DeleteDestinationIfSourceDeleted]
The sourceNode is the "player" object, and the destination node is the "avatar" object. The edge name is "avatar", and the edge nodeDeleteRules specify that if the player object (source) is deleted, then the avatar object (destination) should be automatically deleted.
If it is more convenient for you, you could easily create the same edge in reverse, and get the exact same effect:
(avatar) ----------> (player) @"player"[YDB_DeleteSourceIfDestinationDeleted]
It does not make any difference which way you do it. Whichever is easiest, or most convenient for you.
### Example 2: Parent -> children relationships (one-to-many)This is really the exact same as the example above. You just create multiple edges. For example, the parent object is a "Contact", and the child objects are "PhoneNumber"s. Because a contact may have multiple phone numbers.
So you could create the edges like this:
(contact) ----------> (phoneNumber1) @"number"[YDB_DeleteDestinationIfSourceDeleted] (contact) ----------> (phoneNumber2) @"number"[YDB_DeleteDestinationIfSourceDeleted] (contact) ----------> (phoneNumber3) @"number"[YDB_DeleteDestinationIfSourceDeleted]
And again, you could alternatively do the inverse:
(phoneNumber1) ----------> (contact) @"contact"[YDB_DeleteSourceIfDestinationDeleted] (phoneNumber2) ----------> (contact) @"contact"[YDB_DeleteSourceIfDestinationDeleted] (phoneNumber3) ----------> (contact) @"contact"[YDB_DeleteSourceIfDestinationDeleted]### Example 3: Parent -> child relationship + Notify
Revisiting the first example (parent -> child relationship), what happens if we delete the "avatar"? We know if we delete the "player", then the "avatar" gets automatically deleted. But what about the reverse? This might be important if there is a person.avatarKey property, and we want to nilify that property if the avatar is deleted.
(player) ----------> (avatar) @"avatar",[YDB_DeleteDestinationIfSourceDeleted | YDB_NotifyIfDestinationDeleted]
This is an example of combining rules to meet your needs. Thus if the avatar is deleted, a "notify" method is invoked on the player object which allows you to set the person.avatarKey property to nil.
The "Notify" section will describe the notify method, how it works, and how to implement it.
### Example 4: Reference counting relationshipThere may be times when you place items in the database which have multiple parents. Here are a few examples:
-
You're making a real estate application, and you download neighborhood information on demand. (Think crime statistics, school districts, etc.) You may want to keep this information around until the properties in the neighborhood get removed. And there may be multiple properties in a single neighborhood that reference the info.
-
You download a photo that belongs to a post. But a bunch of other people forwarded/reposted/retweeted the same post, and their post has the same photoId. And you want to automatically delete the photo when all the associated posts expire from the client side database.
These situations are easily handled with YapDatabaseRelationship, using a simple reference counting type scheme. Here's how it works:
(property1) ----------------> (neighborhoodInfo) @"neighborhood"[YDB_DeleteDestinationIfAllSourcesDeleted] (property2) ----------------> (neighborhoodInfo) @"neighborhood"[YDB_DeleteDestinationIfAllSourcesDeleted] (property3) ----------------> (neighborhoodInfo) @"neighborhood"[YDB_DeleteDestinationIfAllSourcesDeleted]
Now let's say that property1 gets deleted (maybe because it was sold, and thus no longer on the market). The extension will look at the edges in the database, and see if there are any others with the same name and destination node. In this case, it will find the edges from property2 & property3. And so the neighborhoodInfo will remain in the database, and only the first edge is deleted. But when property2 & property3 are eventually deleted (and if no other edges were added with the same name, pointing at the same neighborhoodInfo), then the neighborhoodInfo gets automatically deleted.
## Edge creationThere are two ways in which you create an edge:
- Manually add the edge by invoking a method of YapDatabaseRelationship
- Implement the YapDatabaseRelationshipNode protocol for some of your objects
Multiple ways exist for convenience. You can use either. In fact, you can use both at the same time. So perhaps it's easier to manually add edgeA, but easier to use the YapDatabaseRelationshipNode protocol for edgeB. No problem.
### Manual edge creationThere are methods to add & remove edges in YapDatabaseRelationshipTransaction:
@interface YapDatabaseRelationshipTransaction (ReadWrite)
- (void)addEdge:(YapDatabaseRelationshipEdge *)edge;
- (void)removeEdge:(YapDatabaseRelationshipEdge *)edge;
// ...
Both methods take a YapDatabaseRelationshipEdge object, which has a rather straight-forward interface:
@interface YapDatabaseRelationshipEdge : NSObject <NSCoding, NSCopying>
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *sourceKey;
@property (nonatomic, copy, readonly) NSString *sourceCollection;
@property (nonatomic, copy, readonly) NSString *destinationKey;
@property (nonatomic, copy, readonly) NSString *destinationCollection;
@property (nonatomic, assign, readonly) YDB_NodeDeleteRules nodeDeleteRules;
// ...
For manual edge creation, you generally just add the edge at the same time you add the objects. For example:
[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){
[transaction setObject:newPlayer forKey:newPlayer.playerId inCollection:@"players"];
[transaction setObject:newPlayerAvatar forKey:newPlayer.playerId inCollection:@"avatars"];
YapDatabaseRelationshipEdge *edge =
[YapDatabaseRelationshipEdge edgeWithName:@"avatar"
sourceKey:newPlayer.playerId
collection:@"players"
destinationKey:newPlayer.playerId
collection:@"avatars"
nodeDeleteRules:YDB_DeleteDestinationIfSourceDeleted];
[[transaction ext:@"relationships"] addEdge:edge];
}];
Many key/value stores don't have any notion of relationships. Everything in the database has an explicit identifier (collection/key tuple), so if you want to specify a relationship between two objects, it's a rather straight-forward exercise to add it to your data model. For example, consider this relationship:
@interface Song : NSObject <NSCoding>
//...
@property (nonatomic, copy, readwrite) NSString *albumId;
@property (nonatomic, copy, readwrite) NSString *artistId;
// ...
@end
Thus, given a song identifier, you can easily fetch the associated album & artist too:
[databaseConnection readWithBlock:(YapDatabaseReadTransaction *transaction){
Song *song = [transaction objectForKey:songId inCollection:@"songs"];
Album *album = [transaction objectForKey:song.albumId inCollection:@"albums"];
Artist *artist = [transaction objectForKey:song.artistId inCollection:@"artists"];
}];
The YapDatabaseRelationshipNode protocol builds atop this standard pattern. It allows you to keep your data structured in this standard style, and provides a hook for your objects to report back what relationship edges they want. For example, if we were implementing the Song object (from above):
@interface Song : NSObject <NSCoding, YapDatabaseRelationshipNode>
//...
@property (nonatomic, copy, readwrite) NSString *albumId;
@property (nonatomic, copy, readwrite) NSString *artistId;
// ...
@end
@implementation Player
@synthesize albumId;
@synthesize artistId;
// This method gets automatically called when the object is inserted/updated in the database.
- (NSArray *)yapDatabaseRelationshipEdges
{
YapDatabaseRelationshipEdge *albumEdge =
[YapDatabaseRelationshipEdge edgeWithName:@"album"
destinationKey:albumId
collection:@"albums"
nodeDeleteRules:YDB_DeleteDestinationIfAllSourcesDeleted | YDB_DeleteSourceIfDestinationDeleted];
// YDB_DeleteDestinationIfAllSourcesDeleted:
// automatically delete the album if all associated songs are deleted
// YDB_DeleteSourceIfDestinationDeleted
// automatically delete this song if the album is deleted
YapDatabaseRelationshipEdge *artistEdge =
[YapDatabaseRelationshipEdge edgeWithName:@"artist"
destinationKey:artistId
collection:@"artists"
nodeDeleteRules:YDB_DeleteDestinationIfAllSourcesDeleted | YDB_DeleteSourceIfDestinationDeleted];
// YDB_DeleteDestinationIfAllSourcesDeleted:
// automatically delete the artist if all associated songs are deleted
// YDB_DeleteSourceIfDestinationDeleted
// automatically delete this song if the artist is deleted
return @[ albumEdge, artistEdge ];
}
@end
The extension handles invoking the 'yapDatabaseRelationshipEdges' method automatically. That is, if the object conforms to the YapDatabaseRelationshipNode protocol, then the extension will automatically invoke the 'yapDatabaseRelationshipEdges' method when the object is first inserted into the database, and anytime the object is updated in the database. (When you invoke 'setObject:forKey:inCollection' or 'replaceObject:forKey:inCollection:'.)
### Pros & ConsTwo different ways to create an edge? Which do I choose? How do I decide?
Recall that you can use either technique. In fact, you can use both techniques simultaneously. So let's take a look at the pros & cons between the two techniques. This may help to reveal when you may prefer one technique to the other.
Manual edge management is simple & straight-forward. And it tends to work best when the relationship is also simple & straight-forward. In the example above, an edge was manually created from a Player object to the player's Avatar object. You may have noticed that the avatar used the same key as the player:
[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){
[transaction setObject:newPlayer forKey:newPlayer.playerId inCollection:@"players"];
[transaction setObject:newPlayerAvatar forKey:newPlayer.playerId inCollection:@"avatars"];
// ...
The situation is this: Every player has exactly 1 avatar. The key for the avatar is the same as the key for the player. (But the avatars are stored in a different collection.) A player might not have an avatar. In fact this is common for new players, who haven't set their avatar yet. This situation is automatically handled in code. If a database fetch for the avatar returns nil, then a standard placeholder avatar is used. And if an avatar is updated, this simply replaces the previously stored avatar.
So the Player object does not store an avatarId (not needed), and doesn't have any idea if an avatar exists (also not needed). Thus it may not be fitting to use the YapDatabaseRelationshipNode protocol within the Player object to create the edge. As for the avatar, this is actually just a UIImage. (Yes, you can directly store a UIImage in the database. UIImage conforms to the NSCoding protocol.) So we can't implement the YapDatabaseRelationshipNode protocol within the avatars unless we do something goofy and undesirable like extend UIImage. (If that's even possible.)
So this use case lends itself naturally to manual edge management. If/when we add the avatar for the user, we can simply add the edge at this point.
But not all relationships are so simple...
The beauty of the YapDatabaseRelationshipNode protocol is that the code that creates the edges for you is intimately tied to your existing object model. In the example above, we had a Song object. And this object already had properties for the 'albumId' and 'artistId'. This means that you can use the YapDatabaseRelationshipNode protocol, implement the 'yapDatabaseRelationshipEdges' method within the Song object, and this code handles creating the proper edges for you everywhere. So if there are 10 different places in your code where a Song may get created and added to the database, you don't have to worry about each spot creating the proper edges too. That's automatically handled for you by the 'yapDatabaseRelationshipEdges' method.
There are some other cool things that the YapDatabaseRelationshipNode protocol can do for you too.
Perhaps you already have code that manually does the equivalent of an edge with nodeDeleteRules. That is, you have some logic where if something gets deleted, you check the database somehow, and calculate if something else should get deleted. Following from the example above, if a song gets deleted then you check the database to see if that was the last song in the album. And if so then you delete the album too.
Now you decide to switch to using the relationship extension. You'd like it to automatically handle these delete tasks for you. That way you can just delete the song, and the extension will automatically handle deleting the album (and artist) if needed. This will greatly simplify your own code. And you can delete a bunch of code too. But how to make the transition? The database already has a bunch of songs in it... Which means edges need to be created for all these existing songs...
The YapDatabaseRelationshipNode protocol (and YapDatabaseRelationship extension) can greatly simplify this task. When you first create and register the YapDatabaseRelationship extension, it will automatically populate its list of edges by invoking the 'yapDatabaseRelationshipEdges' methods on objects already in the database. That is, it will automatically enumerate your Song objects, and populate its list of edges. So you get this transition "for free". If you were using manual edge management, you'd have to write this populate code yourself.
What happens if the user edits the song information, and changes the artist?
If using manual edge management, you will need to remove the old edge (song->oldArtist), and add the new edge (song->newArtist).
What happens when I manually remove the edge?
Manually removing an edge is treated similarly to the situation in which the source node was deleted. In this case, the nodeDeleteRules will run as if the source node was deleted, and delete the oldArtist if there are no other edges pointing to it.
If using the YapDatabaseRelationshipNode protocol, you needn't do much. In fact, all you need to do is change the song.artistId property. Everything else is handled for you.
[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){
Song *song = [transaction objectForKey:songId inCollection:@"songs"];
song.artistId = newArtistId;
[transaction setObject:song forKey:songId inCollection:@"songs"];
// At this point, the YapDatabaseRelationship extension will query the song for its edges.
// That is, it will invoke the yapDatabaseRelationshipEdges method.
// The updated song object will return a different set of edges than it did previously.
// The relationship extension will notice this change, delete the old edge,
// and add the new one automatically.
}];
And again, deleting the old edge (song->oldArtist) will automatically run the nodeDeleteRules as if the source was deleted. Meaning it will "do the right thing" and delete the oldArtist if there were no other songs pointing to it.
## Querying for edgesYou can query the database to find particular edges using any combination of of the following:
- edge name
- source node
- destination node
For example, if you wanted to find all phone numbers for a particular contact:
// Get the contact, and all phone numbers
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
// Fetch the contact
Contact *contact = [transaction objectForKey:contactId inCollection:@"contacts"];
// Fetch all the phone numbers for the contact
[[transaction ext:@"relationships"] enumerateEdgesWithName:@"number"
sourceKey:contactId
collection:@"contacts"
usingBlock:(YapDatabaseRelationshipEdge *edge, BOOL *stop){
[edges addObject:edge];
}]
}];
See YapDatabaseRelationshipTransaction.h for more enumerate methods. There are also similar versions of these methods if you just want to grab the edge count.
## Differences with Core DataSince many developers are familiar with Core Data, it's instructive to point out some of the differences.
In Core Data, you use the model editor to add a relationship to an entity. In doing so you specify what type of entity the relationship points to (it's class). And you are highly encouraged to create an inverse relationship. Which means editing the destination entity to point back to the source entity.
Some people mistakenly translate this as two edges. One from the source to the destination. And another from the destination back to the source. This is a mistake.
With YapDatabaseRelationship there's no need to create inverse edges.
Yes, the edge is "directional". But that doesn't mean anything. You can "traverse" the edge in either direction. So if you have the destination node, you can get the edge, and find the source node.
Edges are directional so code can better index and optimize things. It makes it easier to run queries when edges are directional, and you can specify either the source or destination. And I think you'll find that directional edges fit nicely into your mental model of the data graph.
But if I don't specify an inverse edge, then how do I specify the inverse delete rule?
The nodeDeleteRules of an edge are bi-directional.
So even though an edge is "directional", you can specify what should happen if:
- The source node is deleted
- The destination node is deleted
To illustrate this concept, let's revisit some code from an example above. We were creating a database of songs, and we stored both Song objects & Album objects. The song object created an edge to the album object. However, the node delete rules specified the following:
- If the song is deleted, then delete the album IF there are no other songs pointing to the album
- If the album is deleted, then delete the song(s)
The code looked like this:
- (NSArray *)yapDatabaseRelationshipEdges
{
YapDatabaseRelationshipEdge *albumEdge = // (artist) --> (album)
[YapDatabaseRelationshipEdge edgeWithName:@"album"
destinationKey:albumId
collection:@"albums"
nodeDeleteRules:YDB_DeleteDestinationIfAllSourcesDeleted | YDB_DeleteSourceIfDestinationDeleted];
// YDB_DeleteDestinationIfAllSourcesDeleted:
// automatically delete the album if all associated songs are deleted
// YDB_DeleteSourceIfDestinationDeleted
// automatically delete this song if the album is deleted
// ...
return @[ albumEdge, artistEdge ];
}
Let's continue with this Song & Album example to illustrate another difference. If we were modeling the Album entity in Core Data, we would specify a one-to-many relationship. Thus we might end up with something like this:
@interface Album : NSManagedObject
@property (nonatomic, strong, readwrite) NSSet *songs;
// ...
In YapDatabaseRelationship, when modeling a to-many relationship, you don't have to store the set of identifiers (if you don't want to).
For example, if you were modeling the Album object for YapDatabase, you could choose not to store the list of songs. In fact, doing so is likely just useless overhead (both in terms of storage space, and additional work required on your part to always keep both the Song & Album objects in-sync).
Instead you have several other options to get the list of songs for a given album. One option is simply to query the relationship extension, and ask it to enumerate all the edges that are pointing to the Album:
[databaseTransaction readWithBlock:^(YapDatabaseReadTransaction *transaction){
// Get the album
Album *album = [transaction objectForKey:albumId inCollection:@"albums"];
// Get all the songs
NSMutableArray *songs = [NSMutableArray array];
[[transaction ext:@"relationship"] enumerateEdgesWithName:@"album"
destinationKey:albumId
collection:@"albums"
usingBlock:^(YapDatabaseRelationshipEdge *edge, BOOL *stop){
Song *song = [transaction objectForKey:edge.sourceKey inCollection:edge.sourceCollection];
[songs addObject:song];
}];
}];
This is one option. Probably not even the best one. Most likely you're planing on displaying all the songs in a tableView. Which means the a better solution is to use the YapDatabaseView extension. This could give you a pre-sorted song list for the given album. (And even change notifications so you could animate changes to the list of songs.)
I used Core Data extensively for many years before I created YapDatabase. One of the things I learned is that relationships in Core Data can create a significant amount of overhead. But it's difficult to do without them because simple "objectForKey" lookups aren't so simple in Core Data. A key/value database changes this dramatically. One of the key concepts to understand when switching to YapDatabase is that relationships can be implicit or explicit.
By "implicit relationship" I mean that you don't create an edge (with the relationships extension) between two objects. You just have properties that point to each other. Like this:
@interface Car : NSObject <NSCoding>
@property (nonatomic, strong, readwrite) NSString *makeId; // E.g. points to Ford
@end
So a Car object might have an "implicit relationship" to the Ford object. But there may be no reason to create an explicit graph edge from the Car object to the Ford object. An "explicit relationship" would have a corresponding graph edge.
With YapDatabase, explicit relationships are often unneeded. Implicit relationships handle the majority of use cases (and without any additional overhead). The primary reason to create explicit relationships is to make use of the nodeDeleteRules. If you don't need the nodeDeleteRules, there's usually little reason to create a graph edge. (The query capabilities can usually be handled better by a view or secondary indexing.)
So if you're coming to YapDatabase with a strong background in Core Data, keep in mind that YapDatabase has both implicit and explicit relationships. So for every relationship in your data model, just ask yourself the following question:
Does this relationship require nodeDeleteRules?
If the answer is NO (either because they're not needed, or because you already have code to handle it), then an explicit relationship (graph edge) is unnecessary.
## Graph ProcessingIn order to optimize database access, the YapDatabaseRelationship extension performs its node deletion processing just before the commit. That is, after all the code in your transaction block has finished. For example:
[databaseTransaction readWriteWithTransaction:^(YapDatabaseReadWriteTransaction *transaction){
Player *player = [transaction objectForKey:playerId inCollection:@"players"];
NSString *avatarId = player.avatarKey;
// (player) ----------> (avatar)
// @"avatar",[YDB_DeleteDestinationIfSourceDeleted]
[transaction removeObjectForKey:playerId inCollection:@"players"];
BOOL huh = [transaction hasObjectForKey:avatarId inCollection:@"avatars"];
// huh == YES !!!
// Why?
// Because the YapDatabaseRelationship extension runs the node deletion processing at the very end
// of this transaction, after our code has run.
}];
[databaseTransaction readWriteWithTransaction:^(YapDatabaseReadWriteTransaction *transaction){
BOOL iSee = [transaction hasObjectForKey:avatarId inCollection:@"avatars"];
// iSee == NO
// Because the avatar was deleted in accordance with the nodeDeleteRules of the edge.
}];
This optimization allows the extension to optimize cache access, and also allows it to skip deletion attempts on objects that you manually delete. For example, if you manually delete both the player and related avatar, the extension will see this, and then knows it only needs to remove the edge from the database.
However, you can manually invoke the 'flush' method to force the extension to run all its processing code. This may be useful when testing to ensure everything is getting deleted properly.
[databaseTransaction readWriteWithTransaction:^(YapDatabaseReadWriteTransaction *transaction){
Player *player = [transaction objectForKey:playerId inCollection:@"players"];
NSString *avatarId = player.avatarKey;
// (player) ----------> (avatar)
// @"avatar",[YDB_DeleteSourceIfDestinationDeleted]
[transaction removeObjectForKey:playerId inCollection:@"players"];
[[transaction ext:@"relationships"] flush]; // run node deletion processing now
BOOL iSee = [transaction hasObjectForKey:avatarId inCollection:@"avatars"];
// iSee == NO
// Because the avatar was deleted in accordance with the nodeDeleteRules of the edge.
}];
One other thing to note about delayed graph processing: It allows to you create edges to nodes that don't exist yet. For example:
[databaseTransaction readWriteWithTransaction:^(YapDatabaseReadWriteTransaction *transaction){
// Add the player to the database
[transaction setObject:player forKey:player.playerId inCollection:@"players"];
// At the point that we add the object to the database above,
// the relationships extension will automatically query the player object for its edges.
// Now, at this point, it will return an edge that points to the avatar.
// But we haven't added the avatar to the database yet. Is that OK?
// The answer is YES. Because the extension will wait until our transaction completes before
// requiring the edge node actually exists.
// So as long as we also add the avatar during this transaction, everything works fine.
[transaction setObject:avatar forKey:avatar.avatarId inCollection:@"avatars"];
}];