Fork me on GitHub
#fulcro
<
2021-06-22
>
ysmiraak03:06:37

hello, first of all thank you all for creating and improving fulcro! i'm still learning it, and i have a question: normalization replaces references with idents, and denormalization joins on idents. is there a way to use something other than an ident? say we have lots of [:uuid/id uuid], because of situations like

{:user/id [:uuid/id uuid]}
{:boss/id [:uuid/id uuid]}
...
where we would like to replace the ident with just the uuid. can we make a disptach from simpler values into idents?
- uuid -> [:uuid/id uuid]
- key  -> [:kwid/id  key]
- ...
this way, we could simply have [:a :b :c] instead of [[:kwid/id :a] [:kwid/id :b] [:kwid/id :c]]

tony.kay03:06:17

short answer: no. Longer answer: • A list of keywords is a valid value for an attribute. There is already a chance you could conflict by wanting to have a scalar value of an ident-looking thing, but a vector of keywords or UUIDs is too easy to run into. • Union queries need the first element to dispatch the query • Normalization needs to know which table (ident element 1) and which entity (Id) • You could create a special deftype for an ident to make it special, but that creates all sorts of overheads and hassles, and idents have worked quite well for years without real problems. Also: note that it is very important to use unique ID names. Don’t use :db/id or :uuid/id. You will have problems if you do when you try to actually flex the data model. You want the ID type to be different for every kind of entity. Everything gets better when you do that. Don’t skimp on keywords. Same goes for generalizing a keyword and reusing it in entities (e.g. :entity/name instead of :person/name). The former will cause you all sorts of headaches down the road (think pathom resolution, for example, but also it can cause headaches when optimizing database queries, etc.)

ysmiraak04:06:17

thanks for answering. a problem we met a few times is like this: when two idents [:user/id id] and [:boss/id id] refer to the same entity in db, and the user and boss components require some shared and editable info, we have to keep these two entries in different tables in sync. otherwise we make it [{:user/id [:uuid/id id]}] and [{:boss/id [:uuid/id id]}] so they end up in the same table.

ysmiraak04:06:27

i was trying out rc/nc and hooks, and i wanted to make it so that ui-boss and ui-user etc. are plain functions, and we generate query components depending on data, which are managed in one uuid table. in the process found [:uuid/id ...] everywhere, so wondered if that overhead could be avoided, not just computationally, but also confusing when id beomes [:uuid/id id] when we write mutations to swap on state and the data in state are like {[_ id] :user/id}

ysmiraak05:06:48

we want to make it so that in a react-grid, each card picks component class and query depending on data. the data contains it's own type info [:item/type :type/user] which can be changed to get different views from this item. we could allow both user attributes and boss attributes to be stored in this node, and query for different slices depending on :item/type (among other attributes) as well as for picking the render-fn. with this setup, we ended up moving most stuff into one table, cuz :user/id got replaced with :type/user; and we are now left with [:uuid/id ...] everywhere

ysmiraak05:06:27

i considered deftype as my last option 😄 i would really appreciate if you could explain more about the "overheads and hassles" (apart from that it needs to behave like a size 2 vec)

donavan09:06:10

(I’m certainly a Fulcro beginner so take my comments with a grain of salt) You could have two tables: :boss/id and :user/id . It would require some extra work but if both are returned in a response (even though they point to the same entity in the db, they may have different fields) they’d both be normalised into the separate tables. If you edit common fields you’d have to make sure to edit all entries in the app db. But I would first ask why (say) a :user ’s boss is not a join/relation? Then all :users are in the :user/id table and users that have a boss have a key in their entity (e.g. {:user/id …. :boss {:user/id …}} that joins to the :user/id table

Thomas Moerman09:06:29

"Same goes for generalizing a keyword and reusing it in entities (e.g. :entity/name instead of :person/name)" -> i've been struggling with this question as well, I do this in rare cases e.g. :entity/created-on and :entity/created-by to facilitate generic functions that are part of entity factory functions. However perhaps the missing piece in my approach is that I currently don't maintain a Fulcro RAD-style attribute meta model where you can query the definitions to: find the keyword with a particular meaning for a particular entity type.

Thomas Moerman09:06:15

Generally speaking, attribute-centric domain modeling is a (relatively) novel approach, I'm always interested in design heuristics to avoid common pitfalls etc. A useful reference: https://github.com/souenzzo/eql-style-guide

donavan09:06:57

mmm… I’ve not had to deal with attributes common to all entities but my first thought are abstracting them via a https://book.fulcrologic.com/RAD.html#_referential_attributes but I’m not sure if it will work… just a thought

Jakub Holý (HolyJak)15:06:45

Perhaps take a hint from denormalization experience in RDBMS and do the same? Ie a 3rd table :person/id that both boss and user refer to for the shared attributes.

tony.kay18:06:02

What is the actual entity in the database? It certainly isn't BOTH a boss and a person, so why are you aliasing the data? IMO your problem is one of your own creation: you've used two different names for the same thing.

tony.kay18:06:06

I think perhaps you have a misunderstanding about idents: Any number of components (UI) can use the same ident. So, your Boss component's ident should be :person/id, and :boss/id should not exist period....or if those are really an account, the BOTH Person and Boss should use :account/id.

tony.kay18:06:08

@U052A8RUT I've done the same thing. It sounds like a great idea, and it does have some advantages (e.g. adding a timestamp for created-on is trivial), BUT then you run into things like this: In Datomic a high-population attribute (which :entity/created-on will become) can become very slow to search on, and you cannot include it in a composite tuple targeted to just a specific entity type. Say you want to be able to scan over all invoices by their created-on date. You'd like to have a composite tuple of [:invoice/id :entity/created-on] but that will populate your database with a [nil date] for every non-invoice that has a created-on. The created-by has problems as well, because Pathom will not be able to figure out what the target type is...what's the output spec for a resolver that tries to walk the "created-by" edge?

👍 2
🧠 2
❤️ 2
tony.kay18:06:26

So @U5W5NMF5E, what is the actual database name for the thing that you're loading? It must be in a table or entity if it is really "one" thing. Something like the "boss" indicator is a flag in that table (or perhaps determined by a relation from other entities). But in any case I strongly suspect your problem is aliasing...you're using more than one name for the same thing. Don't do that 😄

tony.kay18:06:40

But the other direction (using the same name for many different things...e.g. the identity of disparate things) is much worse.

Thomas Moerman20:06:37

Very useful advice, thank you very much.

Jakub Holý (HolyJak)21:06:46

Regarding :entity/created-on: if you used a RDBMS with next.jdbc,it would easily load PERSON.CREATED_ON as :person/created-on but if you wanted :entity/created-on, you'd need to add extra code to rename it. Possibly similarly extra work on saving.

roklenarcic05:06:36

I was always wondering how do you disambiguate a random vector with 2 elements, first element being a namespaced keyword, from an ident.

tony.kay05:06:53

The structure of the query. If you are following a join, it can either be a map or ident. Nothing else makes sense.

tony.kay05:06:52

If you query without a join it doesn't matter since you just get the raw value (the vector)

ysmiraak00:06:02

thanks everyone for the inputs! my friends and i want to build a database editor using fulcro, and we want to give users the ability to create new types (tables) and attributes (cols) and then items (rows) of these types. (we studied machine learning together, but we think a better interface between humans and structured information (for data!) is more important at the moment.) we could put items of different types in different tables, e.g. [:type/user uid] , [:type/patient pid] (we work in digital health), and say uid and pid are not the same, at least don't share attributes. but we found no good reason not to fake polymorphism with duck typing when we have RDF databases. so eventually our types turned into just sets of attributes. say we want to make the app available to patients at a station, so now patients (modeled for hospital staff of a station) become users (part of the station). we can simply unify uid and pid, as well as [:type/user uid] and [:type/patient pid] to just [:item/id id], and user related ui components can query for the attributes they need there, while patient related ones query for theirs. but user and patient could share data, e.g. :core/name, :user/email, :item/changes, ... the namespaces we use became about implementations, instead of about data. so by putting all items into one table, we can sync the changes to their (maybe shared) attributes more easily (thanks to fulcro!) i think @U052A8RUT said it right that we are (turned into) attribute-centric domain modeling, but i'd like to think that we are going for effective union types 😄

ysmiraak00:06:59

our database queries always lead from some types (sets of attrs) or attrs or other items, so the search space from :item/id is managed. (i hope? maybe with :item/id split into keys for different partitions in the future?) as for this thread, thanks again for the inputs! fulcro's performance is smooth even with dev settings on. in lots of places we have [[:item/id id1] [:item/id id2] ...] instead of just [id1 id2] as we would like: we actually use keywords as ids! (sorry for the uuid and boss examples, clumsily made up :) we have item ids like :status/pending and :status/done in our code, and they communicate type info in the ns, which is the name of their type keys. our item ids are semantically used as two parts, like idents. in the backend, the main cost we are paying for using keywords as item ids is that queries usually have to go trough this :item/id lookup, a constant overhead, but acceptable for now. in the frontend, since keyword ids are not interned anyways, deftype and friends might be the best option for our situation. maybe a type that behaves like a keyword and size 2 vector and interned.