At this point, you've written a full CRUD app for breweries in the beer-sample database. Another optimization we might want to include is to show the names of beers that belong to a particular brewery. In the relational world, this is typically accomplished using a join between two tables. In Couchbase, the solution is to use a collated view.
Before looking at the map function for this view, it's useful to inspect a beer document.
{ "name": "Old Stock Ale 2004", "abv": 11.4, "ibu": 0, "srm": 0, "upc": 0, "type": "beer", "brewery_id": "north_coast_brewing_company", "updated": "2010-07-22 20:00:20", "description": "", "style": "Old Ale", "category": "British Ale" }
Note the “brewery_id” property. This is the key of a brewery document and can be thought of as a “foreign key.” Note that this type of document foreign key relationship is not enforced by Couchbase.
The basic idea behind a collated view is to produce an index in which the keys are ordered so that a parent id appears first, followed by its children. In the beer-sample case that means a brewery appears in a row followed by rows of beers.
The basic algorithm for the map function is to check the doc.type. If a brewery is found, emit its key (meta.id). If a child is found, emit its parent id (brewery_id). The map function for the view “all_with_beers” is shown below.
function(doc, meta) { switch(doc.type) { case "brewery": emit([meta.id, 0]); break; case "beer": if (doc.name && doc.brewery_id) { emit([doc.brewery_id, doc.name, 1], null); } } }
The trick to ordering properly the parent/child keys is to use a composite key in the index. Parent ids are paired with a 0 and children with a 1. The collated order of the view results is shown conceptually below.
A Brewery, 0 A Brewery, 1 A Brewery, 1 B Brewery, 0 B Brewery, 1
To use Model Views to create this view, simply add an attribute to an overridden Id property on the Brewery class.
[CouchbaseCollatedViewKey("all_with_beers", "beer", "brewery_id", "name")] public override string Id { get; set; }
This is a good time to introduce a simple Beer
class, of which Brewery will have a collection. Create a new model
class named "Beer." For now, include only the
Name property.
public class Beer : ModelBase { public string Name { get; set; } public override string Type { get { return "beer"; } } }
Then add a Beer list property to
Brewery. This property shouldn't be serialized
into the doc, so add the JsonIgnore attribute.
private IList<Beer> _beers = new List<Beer>(); [JsonIgnore] public IList<Beer> Beers { get { return _beers; } set { _beers = value; } }
Since the collated view has a mix of beers and breweries, the
generic GetView<T> method won't work well
for deserializing rows. Instead, we'll use the GetView method that
returns IViewRow instances. First add a new
GetViewRaw method to
RepositoryBase.
protected IView<IViewRow> GetViewRaw(string name) { return _Client.GetView(_designDoc, name); }
Then in BreweryRepository, add a
GetWithBeers method to build the object graph.
This new method performs a range query on the view, starting with
the brewery id and including all possible beer names for that
brewery.
public Tuple<Brewery, bool, string> GetWithBeers(string id) { var rows = GetViewRaw("all_with_beers") .StartKey(new object[] { id, 0 }) .EndKey(new object[] { id, "\uefff", 1 }) .ToArray(); var result = Get(rows[0].ItemId); result.Item1.Beers = rows.Skip(1) .Select(r => new Beer { Id = r.ItemId, Name = r.ViewKey[1].ToString() }) .ToList(); return result; }
Update the Details method of
BreweriesController to use this method.
public ActionResult Details(string id) { var brewery = BreweryRepository.GetWithBeers(id).Item1; return View(brewery); }
Before the closing fieldset tag in details
template, add a block of Razor code to display the beers.
<div class="display-field">Beers</div> <div> @foreach (var item in Model.Beers) { <div style="margin-left:10px;">- @item.Name</div> } </div>