Closed fractlrao closed 2 years ago
@fractlrao On point (2) - isn't this a case for composition? If what is meant is "multiple references to an entity" (and not "multiple instances of entity") a small extension to :ref
should suffice:
(entity :Department
{:Employees {:listof {:ref :Employee.Id}}
...})
If the relationship needs to be expressed as a separate structure, I think it can be achieved by :meta
,
(entity :Department …)
(entity :Employee …)
(entity :DeptEmployee
{:meta {:relationship [:Department :=> :Employee]}
:Department {:ref :Department.Id}
:Employee {:ref :Employee.Id}
:StartDate {:default :Employee.JoinDate}})
I don't know how useful this kind of modelling is - because any data-query that can be done against :DeptEmployee
will work equally well for a join on :Department
and :Employee
. Maybe this can act as a convenient "view" into such joins. Again, a sequence of employee-ids stored in :Department
may have problems with indexing and reverse queries may not give the expected performance.
The relationship construct and the :meta
approach are both aiming to achieve the same. Would we be able to express bi-directional relationship, cardinality and containment with the :meta
approach and still keep it syntactically simple?
It is possible to express bi-directional relationship, cardinality and containment without introducing a new construct to the language. But at this point we can continue to explore more examples/use-cases using the relationship
syntax and try to define its full semantics, then think about simplifying the syntax.
It would help to add declarative constructs to the language. The concept of a dataflow, while being data-oriented, maps 1:1 to functions (except for idempotent operations). To make Fractl more declarative, bolstering features on the entity side of the language (e.g., relationships
, :pre
and :post
invariants) would be helpful.
@fractlrao Is an instance of relationship :DeptEmployee
created automatically when an :Employee
is added to the system? It seems the attributes :WorksFor
and :Workers
are persisted with :Department
and :Employee
. What about the :StartDate
attribute?
Besides the containment use case, the following is a use case where a declarative syntax might be helpful.
In the context of the library example, users checkout and checkin books. It would help to capture this association between :User
and :Book
in a declarative fashion. With a relationship :Checkout
between :User
and ;Book
, in place, a :User
can perform CRUD operations on the relationship. Thereby, checking out (upsert on the relationship) and checking in (delete on the relationship) the book.
Such a declaration will allow the compiler to deduce that a “checkout” action is allowed "on a book by the user”. This information can be used in multiple places - for example, presenting a “checkout” button while displaying a book in the autogenerated UI.
For this to be complete, we will need to ensure that the user can declare :pre
and :post
checks/invariants (e.g., check whether the user has no pending fines (pre) and send out an email on checkout (post))
@vmatv8, I was thinking of the relationship
as something that the user explicitly manages - creates, deletes instances of them, similar to entities.
The relationship construct makes sense in the context of automatic UI generation. I think we need to define the possible or default UI layouts for each type of relationships. In other words, the visual representation for a 1-1, 1-N and N-N relationships. There could be other possible combinations as well - like 2-1, as in the case of accounts->transactions (i.e more specific cases of N-N).
While the approach is attractive, it brings up certain design and implementation challenges as well. Even for the same kind of relationships, the UI generated may have to be very different. Consider the employee-department relationship:
(relationship :EmployeeDepartment
{:meta {:from :Employee :to :Department}
...)
The generated UI can just show a drop-down with all the department-ids in the employee form - a relationship can be established with the upserted-employee and the selected department.
A library-book checkout use-case has the same 1-N relationship between users and books:
(relationship :CheckoutBook
{:meta {:from :User :to :Book}
...)
But the UI for establishing the relationship has to be much complex, probably with multiple screens - because facilities for searching for and listing the books has to be provided.
This means the relationship has to be combined with some kind of "layout templates" to be usable for automated UI generation. @fractlrao Please share your thoughts.
(entity
:Library/User
{:Name :Kernel/String
:DOB :Kernel/DateTime
:DateJoined :Kernel/DateTime})
(entity
:Library/Book
{:Title :Kernel/String
:ISBN {:check isbn?}
:Authors {:listof :Kernel/String}})
(relationship
:Library/CheckoutBook
{:User {:ref :Library/User.Id}
:Book {:ref :Library/Book.Id}
:Date :Kernel/DateTime
:meta
{:from :User
:to :Book
:cardinality {:type :1-many
:exclusive true} ; a book can be checked-out at a time by a single user only
}})
The :CheckoutBook
relationship creates the following simple graph:
User----CheckoutBook--->Book
From this graph, the UI for a dashboard can be auto-generated. This will contain
a User input form because the entity :User
is at the root of the graph. If there are
multiple roots, input forms for each root entity will be laid-out on a grid.
Two actions can be performed from the input form - Create and Find. When a new instance is created or queried (by 'Find'), the input form will switch to a card view, with two possible actions - Edit and Cancel. Edit will display the input form with data loaded from the card. Cancel will return to the empty input view.
Any relationships that can be created from the entity will be available as additional
actions (visually represented as buttons, links or menus). For example, the :User
input
form will have a Checkout Book action below it. Activating this will show an expanded
view with a form for creating the relationship. This form will have the Create, Find and
Find All actions. (Find All will display the results in a table, rather than a card). Additional
actions for creating instances of the :to
element of the relationship also will be available in
this form. For instance, The Checkout Book
form will have a Create New Book action.
This will allow the user to navigate to a new input form which is auto-generated by treating
the :to
entity as the new graph root. (Once the UI drills into the graph, the generated forms
will have an additional Back button which will allow the user to navigate up the hierarchy).
(fractl.component/relationships <component-name>)
Return the relationships graph for the component, structured as a map.
Example -
{:Library/User
{:CheckoutBook :Book}}
(fractl.component/fetch-relationship-schema <rel-name>)
Return the full schema of the relationship.
(fractl.component/crud-event-listener <entity/rel-name> <callback-function>)
If any CRUD relevant to the entity or relationship happens within the graph, invoke the callback.
(The UI generation code can register such a callback and update relevant reagent-atoms
for dynamically updating the view).
Let's look at one more example - where accounting transactions are modelled as a relationship.
(entity
:Acme/Account
{:Title :Kernel/String})
(relationship
:Acme/Transaction
{:Debit {:ref :Acme/Account.Id}
:Credit {:ref :Acme/Account.Id}
:Amount :Kernel/Decimal
:Date :Kernel/DateTime
:meta
{:from :Debit
:to :Credit
:cardinality
{:type :1-1
:exclusive false}}})
As the relationship has its :exclusive
flag set to false
,
multiple instance of transactions with the same :Debit
and :Credit
accounts
are possible. As a relationship prevents self-references, :Debit
and :Credit
cannot be the same account-id. No explicit constraint declarations are required for this.
The following cardinalities are supported in relationships: :1-1
(one-to-one), :1-M
(one-to-many), :M-1
(many-to-one) and :M-M
(many-to-many).
TODO: Provide additional example and usage scenarios
The latest proposal is here - https://github.com/fractl-io/fractl/issues/482#issuecomment-1078736357
The current mechanism to associate an entity instance with another entity instance is a
:ref
.:ref
has some constraints::ref
is a uni-directional relationship - reference from the entity where it is defined to the:ref
ed entity is possible, but not the other way round:ref
does not have a mechanism to specify the structure of the relationship. For example, we might want to specify thatentity :Department
contains instances ofentity :Employee
. Such structures are common in most domains and supporting them can be very useful for domain modeling:ref
captures a relationship. This relationship itself might have a lifecycle and many attributes (created date, modified date, current state - e.g., checkout/returned, checkout_on, returned_on, etc) - it would help to capture such attributes.:ref
somewhat captures the relationship, but isn’t an independent construct to perform CRUD on. (example below)Expanding the concept of
:ref
into a first-class construct in the language would be very helpful.The proposal is to create a construct of a
relationship
- a special kind ofrecord
that captures the relationship between two entities:The definition above should automatically create a
:WorksFor
attribute in:Employee
and:Workers
attribute in:Department
.A separate construct like above allows us to:
:type :contains
implies cascading delete by default):StartDate
)