It’s not always possible or optimal to denormalize relationships like the ShoppingCart example. In many cases, a document will need to reference another document. Depending on how your application expects to do reads and writes, you may want to keep your model in separate documents by using referencing.

Let’s look at an example where referencing might be the best approach. Suppose your application has some social media elements. Users can have friends, and users can post text updates.

One way to model this:

Users as individual documents

Updates as individual documents that reference a user

Friends as an array of keys within a user document

With two users, two updates, we would have 4 documents in Couchbase that look like this:

[ // Key: "7fc5503f-2092-4bac-8c33-65ef5b388f4b" { "friends": [ "c5f05561-9fbf-4ab0-b68f-e392267c0703" ], "name": "Matt Groves", "type": "User" }, // Key: "c5f05561-9fbf-4ab0-b68f-e392267c0703" { "friends": [ ], "name": "Nic Raboy", "type": "User" }, // Key: "5262cf62-eb10-4fdd-87ca-716321405663" { "body": "Nostrum eligendi aspernatur enim repellat culpa.", "postedDate": "2017-02-02T16:19:45.2792288-05:00", "type": "Update", "user": "7fc5503f-2092-4bac-8c33-65ef5b388f4b" }, // Key: "8d710b83-a830-4267-991e-4654671eb14f" { "body": "Autem occaecati quam vel. In aspernatur dolorum.", "postedDate": "2017-02-02T16:19:48.7812386-05:00", "type": "Update", "user": "c5f05561-9fbf-4ab0-b68f-e392267c0703" } ] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 [ // Key: "7fc5503f-2092-4bac-8c33-65ef5b388f4b" { "friends" : [ "c5f05561-9fbf-4ab0-b68f-e392267c0703" ] , "name" : "Matt Groves" , "type" : "User" } , // Key: "c5f05561-9fbf-4ab0-b68f-e392267c0703" { "friends" : [ ] , "name" : "Nic Raboy" , "type" : "User" } , // Key: "5262cf62-eb10-4fdd-87ca-716321405663" { "body" : "Nostrum eligendi aspernatur enim repellat culpa." , "postedDate" : "2017-02-02T16:19:45.2792288-05:00" , "type" : "Update" , "user" : "7fc5503f-2092-4bac-8c33-65ef5b388f4b" } , // Key: "8d710b83-a830-4267-991e-4654671eb14f" { "body" : "Autem occaecati quam vel. In aspernatur dolorum." , "postedDate" : "2017-02-02T16:19:48.7812386-05:00" , "type" : "Update" , "user" : "c5f05561-9fbf-4ab0-b68f-e392267c0703" } ]

I decided to model ‘friends’ as a one-way relationship (like Twitter) for this example, which is why Matt Groves has Nic Raboy as a friend but not vice-versa. (Don’t read too much into this, Nic :).

The way to model this in C# could be:

public class FriendbookUser { public Guid Id { get; set; } public string Name { get; set; } public virtual List Friends { get; set; } } public class Update { public Guid Id { get; set; } public DateTime PostedDate { get; set; } public string Body { get; set; } public virtual FriendbookUser User { get; set; } public Guid UserId { get; set; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class FriendbookUser { public Guid Id { get ; set ; } public string Name { get ; set ; } public virtual List Friends { get ; set ; } } public class Update { public Guid Id { get ; set ; } public DateTime PostedDate { get ; set ; } public string Body { get ; set ; } public virtual FriendbookUser User { get ; set ; } public Guid UserId { get ; set ; } }

The Update to FriendbookUser relationship can be modeled as either a Guid or as another FriendbookUser object. This is an implementation detail. You might prefer one, the other, or both, depending on your application needs and/or how your OR/M works. In either case, the underlying model is the same.

Here’s the mapping I used for these classes in Entity Framework. Your mileage may vary, depending on how you use EF or other OR/M tools. Focus on the underlying model and not the details of the OR/M mapping tool.

public class UpdateMap : EntityTypeConfiguration { public UpdateMap() { this.HasKey(m => m.Id); this.ToTable("FriendBookUpdates"); this.Property(m => m.Body); this.Property(m => m.PostedDate); this.HasRequired(m => m.User) .WithMany() .HasForeignKey(m => m.UserId); } } public class FriendbookUserMap : EntityTypeConfiguration { public FriendbookUserMap() { this.HasKey(m => m.Id); this.ToTable("FriendBookUsers"); this.Property(m => m.Name); this.HasMany(t => t.Friends) .WithMany() .Map(m => { m.MapLeftKey("UserId"); m.MapRightKey("FriendUserId"); m.ToTable("FriendBookUsersFriends"); }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class UpdateMap : EntityTypeConfiguration { public UpdateMap ( ) { this . HasKey ( m = > m . Id ) ; this . ToTable ( "FriendBookUpdates" ) ; this . Property ( m = > m . Body ) ; this . Property ( m = > m . PostedDate ) ; this . HasRequired ( m = > m . User ) . WithMany ( ) . HasForeignKey ( m = > m . UserId ) ; } } public class FriendbookUserMap : EntityTypeConfiguration { public FriendbookUserMap ( ) { this . HasKey ( m = > m . Id ) ; this . ToTable ( "FriendBookUsers" ) ; this . Property ( m = > m . Name ) ; this . HasMany ( t = > t . Friends ) . WithMany ( ) . Map ( m = > { m . MapLeftKey ( "UserId" ) ; m . MapRightKey ( "FriendUserId" ) ; m . ToTable ( "FriendBookUsersFriends" ) ; } ) ; } }

If, instead of storing these entities as separate documents, we applied the same denormalization as the shopping cart example and attempted to store a user and updates in one document, we would end up with some problems.

Duplication of friends : each user would store the details for their friends. This is not tenable, because now a user’s information would be stored in multiple places instead of having a single source of truth (unlike the shopping cart, where having the same item in more than one shopping cart probably doesn’t make any domain sense). This might be okay when using Couchbase as a cache, but not as a primary data store.

: each user would store the details for their friends. This is not tenable, because now a user’s information would be stored in multiple places instead of having a single source of truth (unlike the shopping cart, where having the same item in more than one shopping cart probably doesn’t make any domain sense). This might be okay when using Couchbase as a cache, but not as a primary data store. Size of updates: Over a period of regular use, an individual user could post hundreds or thousands of updates. This could lead to a very large document which could slow down I/O operations. This can be mitigated with Couchbase’s sub-document API, but also note that Couchbase has a ceiling of 20mb per document.

Note: There’s an N+1 problem here too (friends of friends, etc), but I’m not going to spend time on addressing that. It’s a problem that’s not unique to either database.

Additionally, it may not be the case that when the application reads or writes a user that it will need to read or write friends & updates. And, when writing an update, it’s not likely that the application will need to update a user. Since these entities may often be read/written on their own, that indicates that they need to be modeled as separate documents.

Note the array in the Friends field in the user document and the value in the User field in the update document. These values can be used to retrieve the associated documents. Later in this post, I’ll discuss how to do it with key/value operations and how to do it with N1QL.

To sum up, there are two ways to model data in a document database. The shopping cart example used nested objects, while the social media example used separate documents. In those examples, it was relatively straightforward to choose. When you’re making your own modeling decisions, here’s a handy cheat sheet:

Table 2. Modeling Data Cheat Sheet If …​ Then consider…​ Relationship is 1-to-1 or 1-to-many Nested objects Relationship is many-to-1 or many-to-many Separate documents Data reads are mostly parent fields Separate document Data reads are mostly parent + child fields Nested objects Data reads are mostly parent or child (not both) Separate documents Data writes are mostly parent and child (both) Nested objects

Key/value operations To get document(s) in Couchbase, the simplest and fastest way is to ask for them by key. Once you have one of the FriendbookUser documents above, you can then execute another operation to get the associated documents. For instance, I could ask Couchbase to give me the documents for keys 2, 3, and 1031 (as a batch operation). This would give me the documents for each friend. I can then repeat that for Updates , and so on. The benefit to this is speed: key/value operations are very fast in Couchbase, and you will likely be getting values directly from RAM. The drawback is that it involves at least two operations (get FriendbookUser document, then get the Updates). So this may involve some extra coding. It may also require you to think more carefully about how you construct document keys (more on that later).