PurelyFunctional.tv Newsletter 426: Ring for CRUD
Issue 426 - May 10, 2021 · Archives · Subscribe
Follow-up 🙃
Ring's power
After last week's issue about Ring as a method, Alex Miller pointed me to a great talk by the creator of Ring, Mark McGranaghan, from back in 2010. Alex says it was an important part of the Clojure ethos of data orientation.
Clojure Tip 💡
Ring for CRUD
Last week I promised to apply Ring as a method to CRUD. People interested in Clojure have often asked me what the default CRUD framework is. I'm not a fan of CRUD. It seems to pass on all of the complexity of the model to the caller. Bet that as it may, CRUD is proven to be successful in the real world. It's a simple way to build an API that can read and write from the database. What's more, CRUD standardizes its operations in such a way that we can easily build a small data abstraction that solves the core of the problem and has simple extension points to solve the ancillary problems.
It's a bold statement, but Ring (as a method) can provide this kind of solution. (From here on out, when I say Ring, I refer to the method, not the HTTP server library and ecosystem.) I'm not advocating for CRUD as a model for your application. But it does serve as a useful example of the application of Ring. Here are the two parts outlined last week:
- Specify a data abstraction that better suits your purposes than what you're building on (as in the Ring SPEC is better than servlets)
- Allow extension via functions (as in handlers and middleware)
Building this solution will take more than one issue.
First, we define CRUD. CRUD is a pattern for storing, accessing, modifying, and deleting entities of different types.
On the surface, there appear to be four methods (Create, Read, Update, and Delete). But there are actually several more.
- Create - create a new entity, returning its id
- Read - return an entity given the id
- List - return a list of entities, perhaps with filtering
- Replace - completely replace all attributes with a new representation
- Merge - add and replace attributes of an entity
- Delete - delete an existing entity by id
There may be more, but this is a good start. Now that we have the operations, we can begin to build the data abstraction (step 1).
An abstraction defines what information is ignored. I propose we ignore where the operation comes from (HTTP, etc.) and how it is implemented (database, etc.). What that leaves is the essence of the operation. Let's represent the Create operation as a map.
{:operation :create ;; or what-have-you
...} ;; more to come...
In CRUD, we typically have multiple types of entities, so we model that, too. The types of entities are totally domain-specific, so we are flexible here. I use a string.
{:operation :create
:entity-type "user"
...}
We also accept the data for the entity. This is also domain-specific. Typically, in Clojure, we expect it to follow the Entity Pattern and use a map.
{:operation :create
:entity-type "user"
:entity {...} ;; very domain-specific, but typically a map
...}
That's about it for Create. Let's imagine this operation is handled. What do we get back?
{:status :success
:entity-type "user"
:message "Thank you for creating a user."
:id 123
:entity {...}}
:message
is a human-oriented string, just to be friendly.
Note that, up to this point, we have not mentioned web concepts. We haven't talked about URLs or HTTP status codes or request methods. We also haven't talked about database tables, queries, or the like. This abstraction doesn't know about those things. It merely expresses an opinion (in the form of a spec) and delegates the technology choices to other parts. We'll get to that in the next issue.
Before we go, let's give shape to the other operations.
{:operation :read
:entity-type "user"
:id "1221"}
{:operation :list
:entity-type "user"}
{:operation :replace
:entity-type "user"
:id "2321"
:entity {...}}
{:operation :merge
:entity-type "user"
:id "2321"
:entity-data {...}} ;; it's not a complete entity
{:operation :delete
:entity-type "user"
:id "2321"}
I like this because there's an obvious extension point: We can always add more operation types. It also feels nice that the representations are compact.
At the risk of making this issue too long, here are some possible response types. These are some of the domain non-specific responses we should model. I believe there is a lot of value in completely covering that the common situations and leaving room for domain-specific extensions.
{:status :success
:entity-type "user"
:message "Thank you for creating a user."
:id 123
:entity {...} | :entities [{...} {...} ...]}
Anything besides :success
is an error.
If we can't find the entity for a given id:
{:status :entity-not-found
:entity-type "user"
:message "Could not find a user by id 123."
:id 123}
But what if they ask for an entity type that we don't know about? That should be different.
{:status :entity-type-not-found
:entity-type "dog"
:message "The "dog" entity type is unknown."}
{:status :operation-not-allowed
:entity-type "dog"
:operation :list
:message "The "list" operation is not allowed for the "dog" entity
type."}
What if the operation is malformed? Meaning, the data for the entity doesn't validate?
{:status :request-malformed
:entity-type "user"
:message "You forgot to include an :email attribute. It is required for all
users."}
Generic errors can happen, too.
{:status :server-error
:message "I don't know what happened. Sorry!"}
Okay! That's long enough for this week. Next week we'll talk about handlers.
Podcast episode🎙
This week on the podcast, I explore Newtonian mechanics as a powerful domain model. What can it tell us about the nature of abstraction?
Book update ✍️
Many people have been send ing me pictures of their printed copies of Grokking Simplicity! They've been arriving from the printer at readers' houses for the past couple of weeks. It is very gratifying to see people so excited and engaged with my creation.
If you want a copy, you can buy one here. If you use the coupon code TSSIMPLICITY, you will get 50% off, which will more than offset the cost of shipping.
But, if you really like Amazon, please pre-order your copy now. It will be shipped starting May 18. However, it would really help me as an author. Amazon counts all pre-orders as sales on the day of release. And Amazon's algorithm rewards big sales days. So, I'm calling in a favor: If you were going to get it on Amazon anyway, please order it now rather than wait.
Shortly after it launches on Amazon, I will call in another favor: If you like it, a 5-star review!
Quarantine update 😷
I know a lot of people are going through tougher times than I am. If you, for any reason, can't afford my courses, and you think the courses will help you, please hit reply and I will set you up. It's a small gesture I can make, but it might help.
I don't want to shame you or anybody that we should be using this time to work on our skills. The number one priority is your health and safety. I know I haven't been able to work very much, let alone learn some new skill. But if learning Clojure is important to you, and you can't afford it, just hit reply and I'll set you up. Keeping busy can keep us sane.
Stay healthy. Wash your hands. Wear a mask. Take care of loved ones.
Clojure Challenge 🤔
Last issue's challenge
- Closest palindrome integer - Submissions
This week's challenge
Collatz sequence
A Collatz sequence for a positive integer n
is defined by repeatedly
applying the following rules:
- If
n
is even, divide by 2. - If
n
is odd, multiply by 3 and add 1.
The sequence ends when n = 1
.
Write a function that generates a lazy Collatz sequence given a number.
Examples
(collatz 1) ;=> (1)
(collatz 2) ;=> (2 1)
(collatz 3) ;=> (3 10 5 16 4 2 1)
Thanks to this site for the problem idea, where it is rated Hard in Ruby. The problem has been modified.
Please submit your solutions as comments on this gist.
Rock on!
Eric Normand