Reputation: 758
I have a user entity type in my Datomic database which can follow other user types. My issue comes when one user follows another user who is already following them:
User A follows user B and also User B follows user A
When I try to serialize (using Cheshire) I get a StackOverflowError because of (I'm guessing) infinite recursion on the :user/follows-users
attribute.
How would I go about serializing (to json for an API) two Datomic entities that reference each another in such a way?
Here's a basic schema:
; schema
[{:db/id #db/id[:db.part/db]
:db/ident :user/username
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity
:db.install/_attribute :db.part/db}
{:db/id #db/id[:db.part/db]
:db/ident :user/follows-users
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many
:db.install/_attribute :db.part/db}
; create users
{:db/id #db/id[:db.part/user -100000]
:user/username "Cheech"}
{:db/id #db/id[:db.part/user -200000]
:user/username "Chong"}
; create follow relationships
{:db/id #db/id[:db.part/user -100000]
:user/follows-users #db/id[:db.part/user -200000]}
{:db/id #db/id[:db.part/user -200000]
:user/follows-users #db/id[:db.part/user -100000]}]
And once the database is set up etc. on repl:
user=> (use '[cheshire.core :refer :all])
nil
user=> (generate-string (d/touch (d/entity (d/db conn) [:user/username "Cheech"])))
StackOverflowError clojure.lang.RestFn.invoke (RestFn.java:433)
Upvotes: 2
Views: 553
Reputation: 758
I'm a bit of a n00b to Datomic and am certain there must be a more idiomatic way of doing what @arthur-ulfeldt suggests above but in case anyone else is looking for a quick pointer on how to go about serializing Datomic EntityMaps into json where a self-referencing ref exists, here's the code that solves my problem:
(defn should-pack?
"Returns true if the attribute is type
ref with a cardinality of many"
[attr]
(->>
(d/q '[:find ?attr
:in $ ?attr
:where
[?attr :db/valueType ?type]
[?type :db/ident :db.type/ref]
[?attr :db/cardinality ?card]
[?card :db/ident :db.cardinality/many]]
(d/db CONN) attr)
first
empty?
not))
(defn make-serializable
"Stop infinite loops on recursive refs"
[entity]
(def ent (into {} entity))
(doseq [attr ent]
(if (should-pack? (first attr))
(def ent (assoc ent
(first attr)
(map #(get-entity-id %) (first (rest attr)))))))
ent)
Upvotes: 0
Reputation: 91587
The eager expansion of linked data structures is only safe in any language if they are cycle free. An api that promises to "eagerly expand data only until a cycle is found and then switch to linking (by user id)" may be harder to consume reliably than one that never expanded and always returned enough users to follow all the links in the response. For instance the request above could return the JSON:
[{"id": -100000,
"username": "Cheech",
"follows-users": [-200000]}
{"id": -200000,
"username": "Chong",
"follows-users": [-100000]}]
Where the list of selected users is found by reducing walk of the users graph into a set.
Upvotes: 1