The final feature to implement on the brewery CRUD forms is paging. It's important to state up front that paging in Couchbase does not work like paging in a typical RDBMS. Though views have skip and limit filters that could be used create the standard paging experience, it's not advisable to take this approach.
The skip filter still results in a read of index data starting with the first row of the index. For example, if an index has 5000 rows and skip is set to 500 and limit is set to 50, 500 records are read and 50 returned. Instead, linked-list style pagination is the recommended approach. Paging should also consider the document ids because keys may collide. However, in the breweries example, paging on name is safe because name is the source of the unique key.
First add an HTML footer to the list table in the Index view, right before the final closing table tag. There is a link back to the first page and links to the previous and next pages. A default page size of 10 is also used. Each time the page is rendered, it sets the previous key to the start key of the previous page. The next key will be explained shortly.
<tr> <td colspan="4"> @Html.ActionLink("List", "Index", new { pagesize = 10 }) @Html.ActionLink("< Previous", "Index", new { startKey = Request["previousKey"], pagesize = Request["pagesize"] ?? "10" }) @Html.ActionLink("Next >", "Index", new { startKey = ViewBag.NextStartKey, previousKey = ViewBag.StartKey, pagesize = Request["pagesize"] ?? "10"}) </td> </tr>
Modify the GetAllByName method in
BreweryRepository to be able to handle range
queries (startkey, endkey).
public IEnumerable<Brewery> GetAllByName(string startKey = null, string endKey = null, int limit = 0, bool allowStale = false) { var view = GetView("by_name"); if (limit > 0) view.Limit(limit); if (! allowStale) view.Stale(StaleMode.False); if (! string.IsNullOrEmpty(startKey)) view.StartKey(startKey); if (! string.IsNullOrEmpty(endKey)) view.StartKey(endKey); return view; }
For the actual paging, modify the
BreweryController's Index method to keep track
of pages. The trick is to select page size + 1 from the view. The
last element is not rendered, but its key is used as the start key
of the next page. In simpler terms, the start key of the current
page is the next page's previous key. The last element's key is
not displayed, but is used as the next page's start key.
public ActionResult Index(string startKey, string nextKey, int pageSize = 25) { var breweries = BreweryRepository.GetAllByName(startKey: startKey, limit: pageSize+1); ViewBag.StartKey = breweries.ElementAt(0).Name; ViewBag.NextStartKey = breweries.ElementAt(breweries.Count()-1).Name; return View(breweries.Take(pageSize)); }
At this point, breweries may be created, detailed (with Children), listed, updated and deleted. The next step is to look at the brewery data from a different perspective, namely location.
Brewery documents have multiple properties related to their location. There are state and city properties, as well as detailed geospatial data. The first question to ask of the data is how many breweries exist for a given country. Then within each country, the counts can be refined to see how many breweries are in a given state, then city and finally zip code. All of these questions will be answered by the same view.
Create a view named “by_country” with the code below. This view will not consider documents that don’t have all location properties. The reason for this restriction is so that counts are accurate as you drill into the data.
function (doc, meta) { if (doc.country && doc.state && doc.city && doc.code) { emit([doc.country, doc.state, doc.city, doc.code], null); } }
For this view, you’ll also want a reduce function, which will count the number of rows for a particular grouping by counting how many rows appear for that grouping. So for example, when the group_level parameter is set to 2 brewery counts will be returned by city and state. For an analogy, think of a SQL statement selecting a COUNT(*) and having a GROUP BY clause with city and state columns.
Couchbase has three built in reduce functions - _count, _sum and _stats. For this view, _count and _sum will perform the same duties. Emitting a 1 as a value means that _sum would sum the 1s for a grouping. _count would simply count 1 for each row, even with a null value.
If you are using Model Views, then simply add
CouchbaseViewKeyCount attributes to each of the
properties that should be produced in the view.
[CouchbaseViewKeyCount("by_country", "country", 0, null)] public string Country { get; set; } [CouchbaseViewKeyCount("by_country", "state", 1)] public string State { get; set; } [CouchbaseViewKeyCount("by_country", "city", 2)] public string City { get; set; } [CouchbaseViewKeyCount("by_country", "code", 3)] public string Code { get; set; }
This view demonstrates how to create ordered, composite keys from domain object properties using the Model Views framework.
The next step is to modify the
BreweryRepository to include methods that will
return aggregated results grouped at the appropriate levels.
This new method will return key value pairs where the key is the
lowest grouped part of the key and the value is the count. Also
add an enum for group levels.
public IEnumerable<KeyValuePair<string, int>> GetGroupedByLocation(BreweryGroupLevels groupLevel, string[] keys = null) { var view = GetViewRaw("by_country") .Group(true) .GroupAt((int)groupLevel); if (keys != null) { view.StartKey(keys); view.EndKey(keys.Concat(new string[] { "\uefff" })); } foreach (var item in view) { var key = item.ViewKey[(int)groupLevel-1].ToString(); var value = Convert.ToInt32(item.Info["value"]); yield return new KeyValuePair<string, int>(key, value); } }
Create a new controller named "CountriesController" to contain the actions for the new grouped queries. Use the empty controller template.
Modify the new controller to include the code below, which sets up
the BreweryRepositoryReference and loads sends
the view results to the MVC View.
public class CountriesController : Controller { public BreweryRepository BreweryRepository { get; set; } public CountriesController() { BreweryRepository = new BreweryRepository(); } public ActionResult Index() { var grouped = BreweryRepository.GetGroupedByLocation(BreweryGroupLevels.Country); return View(grouped); } }
Next create a new directory under “Views” named “Countries.” Add a view named “Index” that is not strongly typed.
To the new view, add the Razor code below, which will simply display the keys and values as a list. It also links to the Provinces action, which you’ll create next.
@model dynamic <h2>Brewery counts by country</h2> <ul> @foreach (KeyValuePair<string, int> item in Model) { <li> @Html.ActionLink(item.Key, "Provinces", new { country = item.Key}) (@item.Value) </li> } </ul>
Build and run your application and you should see a page like below.
Next, add the Provinces action to the
CountriesController. This action will reuse the
repository method, but will change the group level to Province (2)
and pass the selected country to be used as a key to limit the
query results.
public ActionResult Provinces(string country) { var grouped = BreweryRepository.GetGroupedByLocation( BreweryGroupLevels.Province, new string[] { country } ); return View(grouped); }
Create another empty view named “Provinces” in the “Countries” directory under the “Views” directory. Include the content below, which is similar to the index content.
@model dynamic <h2>Brewery counts by province in @Request["country"]</h2> <ul> @foreach (KeyValuePair<string, int> item in Model) { <li> @Html.ActionLink(item.Key, "Cities", new { country = Request["country"], province = item.Key}) (@item.Value) </li> } </ul>
Compile and run the app. You should see the Provinces page below.
Creating the actions and views for cities and codes is a similar
process. Modify CountriesController to include
new action methods as shown below.
public ActionResult Cities(string country, string province) { var grouped = BreweryRepository.GetGroupedByLocation( BreweryGroupLevels.City, new string[] { country, province }); return View(grouped); } public ActionResult Codes(string country, string province, string city) { var grouped = BreweryRepository.GetGroupedByLocation( BreweryGroupLevels.PostalCode, new string[] { country, province, city }); return View(grouped); }
Then add a view named "Cities" with the Razor code below.
@model dynamic <h2>Breweries counts by city in @Request["province"], @Request["country"]</h2> <ul> @foreach (KeyValuePair<string, int> item in Model) { <li> @Html.ActionLink(item.Key, "Codes", new { country = Request["country"], province = Request["province"], city = item.Key}) (@item.Value) </li> } </ul>
Then add a view named "Codes" with the Razor code below.
@model dynamic <h2>Brewery counts by postal code in @Request["city"], @Request["province"], @Request["country"]</h2> <ul> @foreach (KeyValuePair<string, int> item in Model) { <li> @Html.ActionLink(item.Key, "Details", new { country = Request["country"], province = Request["province"], city = Request["city"], code = item.Key}) (@item.Value) </li> } </ul> @Html.ActionLink("Back to Country List", "Index")
Compile and run the app. Navigate through the country and province listings to the cities listing. You should see the page below.
Click through to the codes page and you should see the page below.
The last step for this feature is to display the list of breweries
for a given zip code. To implement this page, you need to add a
new method to BreweryRepository named
GetByLocation. This method will use the same
view that we’ve been using, except it won’t execute the reduce
step. Not executing the reduce step means that the results come
back ungrouped and individual items are returned.
public IEnumerable<Brewery> GetByLocation(string country, string province, string city, string code) { return GetView("by_country").Key(new string[] { country, province, city, code }).Reduce(false); }
Then add a Details action method to the
BreweriesController that calls this method and
returns its results to the view.
public ActionResult Details(string country, string province, string city, string code) { var breweries = BreweryRepository.GetByLocation(country, province, city, code); return View(breweries); }
Create a Details view in the “Countries” folder with the Razor code below.
@model IEnumerable<CouchbaseBeersWeb.Models.Brewery> <h2>Breweries in @Request["code"], @Request["city"], @Request["province"], @Request["country"]</h2> <ul> @foreach (var item in Model) { <li> @Html.ActionLink(item.Name, "Details", "Breweries", new { id = item.Id }, new { }) </li> } </ul> @Html.ActionLink("Back to Country List", "Index")
Compile and run the app. Click through country, province and state on to the Codes view. The code above already has a link to this new Details page. When you click on a postal code, you should see a list of breweries as below.