Separate Entities for Relationships Or No?


#1

In a previous question, I asked about creating a relationship entity.

In a traditional RDBMS, I would create a many-to-many table for this kind of thing and then join them as needed, but nothing in the tables being joined. Simple case user table:

[1] Bob
[2] Fred

Simple relationship table:

[1] "works for" [2]

I wouldn’t put anything about the relationships in the user table. But it’s sort of tempting with Datomic to have the user entity have an attribute refers to many relationships.

It’s a bad idea to do this in an RDBMS because if you put the information that “Fred works for Bob” in Fred’s record, Bob’s record must also be updated to say “Bob is the boss of Fred”.

But in a logical programming language, you get “is the boss of” for free when you specify “works for” (presuming you’ve defined that). And I feel like with the whole EAV thing it probably shouldn’t matter, since there isn’t really a record in that sense.

Am I getting this?


#2

Well, I guess the answer to that is here but it does make me nervous. :grimacing:


#3

Don’t be nervous! A big value prop of modeling the domain as facts like
[bob :orgchart/works-for fred]
is that we can then later ask all sorts of questions like
[?e :orgchart/works-for fred] and
[bob :orgchart/works-for ?e].
It all follows the same general pattern and is very robust and simple.


#4

Thanks for the words of encouragement. Your description is how I think of logic programming but I don’t think I’ve expressed it optimally in Datomic. I switched from the concept above to have the relationship schema just be:

[“works for”][“Bob”]

And the person schema to have a reference to many relationships. So when I pull up Fred, I automatically see that he has a relationship to Bob, but when I pull up Bob I see nothing. To figure out who works for Bob, I end up querying every relationship where Bob is the target, then querying for who has that relationship.

(defn relationships-to-user [id]
(qf `[:find ?e :where [?e :relationship/party ~id]]))

(defn users-with-relationships [rels]
(map #(qff `[:find ?e :where [?e :user/relationships ~%]]) rels))

(defn user-by-relationship [id]
(map user (users-with-relationships (relationships-to-user id))))

I’m pretty sure this isn’t the best. I’m not sure why I changed it from [A][relation][B], but it seemed like a good idea at the time.

I’m really sure that this querying isn’t right but I haven’t figure out more complex queries.


#5

First of all some meta tips:

  1. I don’t recommend you make wrapper helpers like qff, it’s a rookie instinct but there are better ways
  2. Same with splicing constants into query - faster to bind them with :in

(d/q '[:find ?e . :in $ ?f :where [?e :user/relationships ?f]] $ id)

The direction of the relationship I don’t think matters other than what you see by default. Given fred you pull :user/relationships to get bob. Given bob you pull backwards :user/_relationships to get fred. You won’t see backwards relationships unless you ask to see them.


#6
  1. I don’t recommend you make wrapper helpers like qff, it’s a rookie instinct but there are better ways

Oh, I inherited it and I hate it instinctually—though you’re right, the guys who did it were also rookies—but I didn’t want to just throw it out without trying it. I basically inherited a macro that creates a bunch of namespaced variables and functions like that which, well, first it’s a big ugly macro (which I’ve personally never had to create in four years of Clojure), second creates a layer between all the instruction on the internet—like, my entity definitions look like this:

(defent user
    :db accounts
    :schema [id :uuid :unique-identity]
    [email :string :unique-identity :indexed]
    [first-name :string]

—and whenever I want to do something not dreamed of in the macro, I can’t just look at the docs, I have to dig around the macro first to figure out what it does, and last ends up with me maintaining the macro instead of the code.

So, yeah, anything you can point me to on how those things are usually handled is welcome.

  1. Same with splicing constants into query - faster to bind them with :in

Cool. I’ll do that and start phasing out the macro.

Given fred you pull :user/relationships to get bob.

I get that.

Given bob you pull backwards :user/_relationships to get fred.

And that’s what I’m doing (I think, just with the three calls noted previously). But I feel like relationships doesn’t even need to be part of the User entity.

I mean, if I have:

[party-A][relation][party-B]

That should be enough, right? Then my queries would be something like:

[Bob][employs][?]

To get all of Bob’s employees and:

[?][employs][Bob]

To get all of Bob’s bosses. (I mean, I’ll end up making multiple calls either way 'cause I don’t have it down, but eventually I’m sure I can get this into one call.)

Thanks a lot, Dustin!


#7

I made this, hope it helps: http://www.dustingetz.com/:datomic-find-specifications/

You are right that once the fact is asserted, you can use datalog to query the facts. Datomic has the datomic.api/q API for querying like this. There is also datomic.api/pull, which is oriented towards graph traversals rather than asking questions about facts. So once we assert [bob :employs fred], we can ask fact-oriented questions like
(d/q '[:find [?e ...] :in $ ?u :where [?e :user/employs ?u]] db fred)
or we can also cast the same question to be framed as a traversal from fred:
(d/pull db [:user/_employs] fred).