This ASP.NET Core CRUD series is coming to end. We’ve covered setup (part 1), reading with SQL++ (part 2), reading with key-value (part 3), and creating/updating (part 4). In this last post, we’ll look at the D in CRUD: deleting.
Deleting with SQL++ or Key-Value
Hopefully you’re noticing a pattern. Just as with reading, creating, and updating, you’ve got multiple paths for deleting. You can use a SQL++ DELETE statement:
1 2 |
DELETE FROM demo._default.wishlist w WHERE META(w).id = "1c3de2e7-70ea-4ee2-803b-425bbf6251cb" |
Or you can use a key-value delete operation (aka “Remove“):
1 |
await collection.RemoveAsync("1c3de2e7-70ea-4ee2-803b-425bbf6251cb"); |
And as before, here are the very similar guidelines to help you decide which one to use:
Use case | Key-value? | Why or why not? |
Delete a user with key “73892” | Yes | Direct access |
Delete a group of users with keys “73892”, “47212”, and “90491” | Yes | This may require multiple key-value operations, but this still may be faster than using a SQL DELETE …​ WHERE …​ IN query. |
Delete every user from “Ohio” | No | User’s state is likely a “secondary” attribute, not a key (multiple users can be from Ohio). This is a good use case for a SQL++ DELETE |
A Delete endpoint for ASP.NET Core
With that in mind, create a delete endpoint like so:
1 2 3 4 5 6 7 8 9 10 11 |
[HttpDelete] [Route("api/delete")] public async Task<IActionResult> Delete(Guid id) { Â Â var bucket = await _bucketProvider.GetBucketAsync("demo"); Â Â var collection = await bucket.CollectionAsync("wishlist"); Â Â await collection.RemoveAsync(id.ToString()); Â Â return Ok(new { success = true }); } |
Try this endpoint with OpenAPI / Swagger, and it will behave how you’d expect.
Should I really delete?
In many use cases, you may not want to actually delete data. You may want to do something called a “soft” delete. This involves moving or marking the data in such a way that it still exists in the database, but it is no longer shown to the end user. This has the benefit of being discoverable, recoverable, and reportable.
It’s also a great opportunity to show the flexibility of a JSON NoSQL database.
Soft-deleting
Let’s introduce a “deleted” field to wishlist items. This field will contain a date/time that the item was deleted. If this field exists, the rest of the endpoint should consider this item to be deleted. However, if we need to, we can still query it, report on it, and recover it.
In a relational database, this would likely require an ALTER. Depending on the size of your data, it could require some downtime, or possibly a lot of NULL values. In a JSON database, we don’t need to tell the database anything about a new field.
Let’s use the subdocument API to add a “deleted” field to the document. Sub-document means that we will only operate on a portion of the JSON, and leave the rest alone.
1 2 3 4 5 6 7 8 9 10 11 12 |
[HttpDelete] [Route("api/softDelete")] public async Task<IActionResult> SoftDelete(Guid id) { Â Â var bucket = await _bucketProvider.GetBucketAsync("demo"); Â Â var collection = await bucket.CollectionAsync("wishlist"); Â Â await collection.MutateInAsync(id.ToString(), Â Â Â Â options => options.Upsert("deleted", DateTime.Now)); Â Â return Ok(new { success = true }); } |
(Make sure using Couchbase.KeyValue; is at the top of your GiftsController file.)
This code sends a command to Couchbase: for the document with such-and-such ID, upsert a field called “deleted” and give it the current date/time as a value.
Notice that with sub-document, we didn’t have to first load the existing document, and we didn’t have to send the entire modified document back over the wire.
Soft-deleted data
The end result will be a document that looks like this:
1 2 3 4 |
{ Â "name": "Digital Photo Frame", Â "deleted": "2022-04-21T11:05:26.1766248-04:00" } |
The other documents in my wishlist do not have a deleted field. They still look like:
1 2 3 |
{ Â "name": "Skyline Chili 2XL T-Shirt" } |
and
1 2 3 |
{ Â "name": "Joey Votto jersey" } |
Note that they don’t have a “deleted”: null field; they don’t have a “deleted“ field at all.
Soft-deleted SELECT
The data is marked as deleted, but it’s still in the database. We need to modify the GetAll endpoint (see part 2 for more about GetAll) to take this into account:
1 2 3 |
SELECT META(w).id, w.* FROM demo._default.wishlist w WHERE w.deleted IS MISSING |
I’ve introduced some more SQL++ syntax here: MISSING. This is a concept that doesn’t exist in relational databases. In relational, any column specified in the query must be defined and must have a value (even if it’s null). With a JSON NoSQL document database, there is no such constraint.
Improving the index
One last point to discuss is indexing. Back in part 2, we created a primary index just to get started. However, that index will rarely be the most efficient. Creating and tuning indexes is a deep topic, just as it is in the relational database world.
Fortunately, Couchbase Capella has a built-in Advise tool that can recommend better indexes. Just click Advise in the Query Workbench (or you can use ADVISE syntax).
In this case, it gives the following recommendation:
1 2 3 4 5 6 7 8 9 10 11 |
"recommended_indexes": { Â Â Â Â Â "indexes": [ Â Â Â Â Â Â { Â Â Â Â Â Â Â "index_statement": "CREATE INDEX adv_deletedISMISSING ON `default`:`demo`.`_default`.`wishlist`(`deleted` IS MISSING) WHERE `deleted` IS MISSING", Â Â Â Â Â Â Â "index_statement_relative": "CREATE INDEX adv_deletedISMISSING ON `wishlist`(`deleted` IS MISSING) WHERE `deleted` IS MISSING", Â Â Â Â Â Â Â "keyspace_alias": "wishlist_w", Â Â Â Â Â Â Â "query_context": "default:demo._default", Â Â Â Â Â Â Â "recommending_rule": "Index keys follow order of predicate types: 2. equality/null/missing." Â Â Â Â Â Â } Â Â Â Â Â ] Â Â Â Â } |
In the case of our very small amount of wishlist data, this index is probably not worth creating. However, if we were managing the wishlists of an entire eCommerce site (for example), this index would be a good starting point.
The end of CRUD
We’ve reached the end of creating a very simple ASP.NET Core CRUD application with Couchbase. The final API surface looks like:
Here is a breakdown of the concepts in this series, with links to documentation to dive deeper:
-
- Setting up Capella
- Setting up .NET dependency injection
- SQL++ (sometimes still called “N1QL”)
-
-
- Key-value CRUD operations
- Sub-document operations
-
The complete source code for this series is available on GitHub.
What’s next?
Sign up for a Capella free-trial. The Couchbase Capella DBaaS is the easiest way to get started with Couchbase, and no credit card is required.
Check out the Couchbase Playground for .NET examples that you can run right in the browser.
Join the Couchbase Discord to ask questions and exchange comments with Couchbase engineers and with other members of the Couchbase community.