Suppose I have this:
user> (def foo [{:id 1 :foo 123} {:id 2 :foo 456}])
#'user/foo
user> (def bar [{:foo_id 1 :bar 111} {:foo_id 1 :bar 222}])
#'user/bar
What I want is to "join" foo and bar so that each item in foo ends up with a sub-list of bars based on matching key fields.
In real life, these lists-of-hash-maps are coming out of a database via clojure.contrib.sql, so this is something I actually want to do pretty often. This is also vaguely similar to what you get out of a Rails-like ORM, where you end up with an object that has lists of sub-objects anywhere you have a one-to-many relationship.
Here's how I end up doing this in Clojure:
(defn one-to-many
([xs name ys f]
(for [x xs :let [ys (filter (partial f x) ys)]]
(assoc x name ys))))
Now I can do this:
user> (pprint (one-to-many foo :bars bar #(= (:id %1) (:foo_id %2))))
({:bars ({:foo_id 1, :bar 111} {:foo_id 1, :bar 222}), :id 1, :foo 123}
{:bars (), :id 2, :foo 456})
And if I define a helper function:
(defn key=
([xkey ykey]
#(= (xkey %1) (ykey %2))))
Then I can write it more concisely:
user> (pprint (one-to-many foo :bars bar (key= :id :foo_id)))
;; same as above
And if I have another "table" of data like this:
user> (def baz [{:foo_id 1 :baz 555} {:foo_id 2 :baz 999}])
#'user/baz
Then I can join them all like this:
user> (pprint (-> foo
(one-to-many :bars bar (key= :id :foo_id))
(one-to-many :bazzes baz (key= :id :foo_id))))
({:bazzes ({:foo_id 1, :baz 555}),
:bars ({:foo_id 1, :bar 111} {:foo_id 1, :bar 222}),
:id 1,
:foo 123}
{:bazzes ({:foo_id 2, :baz 999}), :bars (), :id 2, :foo 456})
This is pretty concise. It may be possible to do it in an even more concise way, (if so, do share). If I was willing to adhere to some Rails-y naming convention for my table names and for the id fields in my tables, I could make this shorter by not having to specify the names of the id fields, but I don't want to go there. It's trivial to write similar functions for a one-to-one relationship, or to use a join-table to "join" two tables with a many-to-many relationship.
I am happily surprised sometimes by how simple it is to roll my own version of things that previously seemed like dark magic. I used Rails for a long time and it seemed like a crapload of code must have gone into making the ORM work. But four lines of code gets me 75% of what I ever needed Rails' ORM for.
This may be more thanks to me opening my eyes a bit than to Clojure being awesome, but either way, I'll take it.
Speak Your Mind
Preview
Commenting Help
Email / Avatar
No HTML allowed!
All HTML is auto-escaped. Use Markdown. Examples:
code in backticks