Fork me on GitHub
#datascript
<
2024-04-16
>
seepel17:04:10

As I’ve been fleshing out my application, I’ve found that I keep ending up having something like an impedance mismatch where I have some functions that take an map, some functions that take a lookup, some functions that return maps, and some functions that return lookups. I don’t always have the right object. Is there a good rule of thumb for when to work with maps vs lookups?

Niki20:04:18

what do you mean return lookups?

seepel20:04:17

I end up with stubs that look like

{:db/id 1}
When what I want is a lookup, so I end up pulling the id, but sometimes the map hasn’t been transacted yet and doesn’t have an id so I need to go hunting for a unique identifier attribute.

Niki20:04:00

Okay I still don’t understand. What do you mean you get stub but want lookup? What do you mean by lookup? You get these from where?

seepel21:04:31

Maybe I’m using the wrong term. What I mean by lookup is a vector where the first element is a keyword for a unique identity attribute, and the second element is the value to find. Here is a contrived example where I run into this is. I first pull an entity, and then later want to pull a related entity by ref attribute.

(def schema
  {:guid {:db/unique :db.unique/identity}
   :child {:db/valueType :db.type/ref}})

(let [lookup [:guid some-guid]
      parent (d/pull db '[*] lookup)
      parent-child (:child parent)
      child (d/pull db '[*] (:db/id child-parent))])
The first pull returns the child as {:db/id } because I didn’t specify it. As a result I have a lot of checks to see if an entity has been pulled already or not when I want to process that entity later. It feels like I’m doing something wrong. Where this really comes in is when the two fetches are separated by time.
(defn pull-parents []
  (d/q '[:find (pull ?e '[*])
         :where [?e :child _]]
       db))

(def pull-child [parent]
  (d/pull db '[* {:child ...}] (:db/id parent)))
On the other side there are transactions where I have an entity and I want to assign another entity to a ref attribute and I end up having to create a “lookup” with a unique identity attribute.
(d/transact conn [{:child [:guid (:guid child))}}])
Hopefully those examples make my question a little more clear.

Niki21:04:31

Ok I think I see

Niki21:04:00

So sometimes you use your own “external” id that needs to be paired with attribute

Niki21:04:10

and sometimes you have entity id

Niki21:04:37

rule of thumb is not to expose entity ids to external systems, even to your own api

Niki21:04:20

so use them in code but if you have better id stored as attribute too then rely on those when talking over module boundaries

seepel21:04:54

Maybe my problem is that I don’t have clear module boundaries. By that do literally mean clojure namespaces or do you mean some abstract module boundary?

Niki21:04:19

abstract

Niki21:04:28

like api

Niki21:04:09

but all in all, just be mindful and maybe have var naming scheme so you remember what is what

Niki21:04:34

I sometimes also not sure if function should accept entity or db + eid

Niki21:04:55

have not figured out rule of thumb there

seepel21:04:24

Got it, I think my question may ultimately boil down to that in the end as well :thinking_face:

Niki21:04:25

or maybe you are talking about that datascript functions should accept {:db/id …} maps where they expect eid or lookup?

Niki21:04:41

I am not sure whether that idea is good or bad

Niki21:04:20

at least what we have now is simple (but not super easy)

seepel21:04:31

For what it’s worth, my vote would be no. I wrote a quick function that would basically check the inputs and find the best value for a given key and fall back to db/id. I found it incredibly difficult to understand what shape my data was in at any given function call.

👍 1
Niki21:04:13

That’s my feeling to

seepel21:04:00

Thanks again for the insights! 🙏

seepel00:05:04

I just discovered why I got so confused. There is an error when trying to transact a nested entity that is also unique. I setup a schema that has one attribute that is a plain old ref and one that is a ref that is also unique.

(def conn (d/create-conn {:unique-ref {:db/valueType :db.type/ref
                                       :db/unique :db.unique/identity}
                          :just-a-ref {:db/valueType :db.type/ref}}))
If I transact the plain old ref with a nested entity there is no problem
(d/transact! conn [{:just-a-ref {:foo 1}}])
{:db-before
 #datascript/DB {:schema
                 {:unique-ref
                  #:db{:valueType :db.type/ref, :unique :db.unique/identity},
                  :just-a-ref #:db{:valueType :db.type/ref}},
                 :datoms []},
 :db-after
 #datascript/DB {:schema
                 {:unique-ref
                  #:db{:valueType :db.type/ref, :unique :db.unique/identity},
                  :just-a-ref #:db{:valueType :db.type/ref}},
                 :datoms [[1 :just-a-ref 2 536870913] [2 :foo 1 536870913]]},
 :tx-data
 [#datascript/Datom [2 :foo 1 536870913 true]
  #datascript/Datom [1 :just-a-ref 2 536870913 true]],
 :tempids {1 1, 2 2, :db/current-tx 536870913},
 :tx-meta nil}
If I try to transact a nested unique ref I get an error that I need to provide an eid or lookup ref.
(d/transact! conn [{:unique-ref {:foo 1}}])
; Execution error (ExceptionInfo) at datascript.db/entid (db.cljc:1204).
; Expected number or lookup ref for entity id, got {:foo 1}
Is this a bug or a necessary constraint?

Sam Ferrell21:04:04

Experience report... mistakenly used an empty map as a value for an attribute whose value type was a reference; seems to work OK until it is serialized and deserialized, at which point it took on the value of a reference to another nearby entity

Sam Ferrell21:04:49

(serialization via datascript-transit)

Niki22:04:29

If you can turn that into an issue with code example I might look into it

Niki22:04:41

Sounds like a check is needed somewhere

Sam Ferrell22:04:51

I'll look into that 👍