-
-
Notifications
You must be signed in to change notification settings - Fork 70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Making entities transactable #48
Comments
This also possibly unlocks use of libraries like specter or meander to update entities. |
Sounds like a good idea. Could you elaborate a bit more on how specter or meander could be used to update entities if the change is made? |
Here's a basic example with specter based on the entities above: (use 'com.rpl.specter)
(def ent-as-map-recursive
{:db/id 1
:user/email "foo@example.com"
:user/friends #{{:db/id 2
:user/email "ava@example.com"} {:db/id 3}}})
(setval [:user/friends ALL :user/email (pred #(= % "ava@example.com"))]
"jenny@example.com"
ent-as-map-recursive)
; =>
{:db/id 1,
:user/email "foo@example.com",
:user/friends #{{:db/id 2, :user/email "jenny@example.com"} {:db/id 3, :user/email nil}}}
(setval [:user/friends ALL :user/email (pred #(= % "ava@example.com"))]
"jenny@example.com"
(d/entity @conn 1)))
; =>
; Execution error (UnsupportedOperationException) at datalevin.impl.entity.Entity/assoc (entity.cljc:47).
; null |
I wonder how to deal with deeply nested, or even recursively nested entities? |
Yes, I started pondering that as well. I actually think that turning the Entity into a map is a bad idea. But we could possible extend the Entity type to hold a change-set of updates to the entity similar to the cache of The key is that the, sic, This would be similar for nested values. Non-updated entities are treated as eids and updated ones contain a minimal change-set derived from assoc/update calls. |
@huahaiy adapted Entity to allow (-> (entity @conn [:user/handle "ava"])
(assoc :user/age 42))
; => {:db/id 1, :<STAGED> #:user{:age [{:op :assoc} 42]}} I like this interface because it is fully backwards compatible. Entity still works read-only but can optionally act as an immutable stage until transacted. Here's a thorough API example: (def db-path "data/lab/entity-db")
(def schema
{:user/handle #:db {:valueType :db.type/string
:unique :db.unique/identity}
:user/address #:db{:valueType :db.type/ref
:cardinality :db.cardinality/one}
:address/street #:db{:valueType :db.type/string}
:user/friends #:db{:valueType :db.type/ref
:cardinality :db.cardinality/many}})
(def conn
(d/create-conn db-path schema))
(transact! conn [{:user/handle "ava"
:user/friends [{:user/handle "fred"}
{:user/handle "jane"}]}])
;; *** Simple example
(let [ava-with-age (-> (entity @conn [:user/handle "ava"])
(assoc :user/age 42))]
#spy/c (entities->txs [ava-with-age])
;; => [[:db/add 1 :user/age 42]]
(transact! conn [ava-with-age])
)
;; *** Nested entities must be transacted separately
(let [{:keys [user/friends] :as ava}
(update (entity @conn [:user/handle "ava"]) :user/age inc)
fred (some
#(when (= (:user/handle %) "fred") %)
friends)
bestie (assoc fred :bestie? true)]
#spy/c (entities->txs [ava bestie])
;; => [[:db/add 1 :user/age 43] [:db/add 2 :bestie? true]]
(transact! conn [ava bestie]))
;; *** `add` and `retract` are directly defined on entity
;; they differ from assoc/dissoc in that they do not overwrite
;; the attr's values
(let [ava (entity @conn [:user/handle "ava"])
fred (some
#(when (= (:user/handle %) "fred") %)
(:user/friends ava))]
#spy/c (entities->txs [(retract ava :user/friends fred)])
;; => [[:db/retract 1 :user/friends 2]]
(transact! conn [(retract ava :user/friends fred)])
) All the code is here: https://github.com/den1k/stuffs/blob/main/src/stuffs/datalevin/entity.clj |
I am all for programmer convenience (that's the whole point of this project), so I think this could be a nice addition. Would you mind sending a PR with some tests as well. Thanks a lot! |
(This feature would mark a diversion from the datascript/datomic API.)
I've always wondered: why are entities not transactable? I find myself converting entities to maps all the time solely to transact them. This still causes problems when entities nest other entities. So here are a few simple ideas on how entities could be treated in transactions:
1. Entities could be treated as refs in transactions
Now I convert it to a map
I try to transact it
So I can either dissoc the
:user/friends
map-entry or convert contained entities to refsWe could spare ourselves from this by treating entities as refs in transactions. The database already walks nested data-structures to resolve refs so why not resolve entities as refs, also?
2. Entities to return maps on update
datalevin.impl.entity/Entity
implementsclojure.lang.Associative
which currently only throws errors:Instead
assoc
could return a hashmapThis would also make
update
possible. Together this means that the change of email toent
from above, could look like this:I would've already implemented this for my own projects but unfortunately Clojure (unlike ClojureScript) doesn't allow to overwrite a Type's methods. To achieve this one would have to for Datalevin and change the definition of
datalevin.impl.entity/Entity
so I wanted to raise the issue here first and see what @huahaiy's thoughts are.The text was updated successfully, but these errors were encountered: