RAD relationships connect entities through reference attributes (type :ref). A relationship's nature is defined by its
cardinality (to-one or to-many), directionality (which entity "owns" the edge), and ownership (whether the
parent exclusively owns the target). These declarations drive resolver generation, save middleware behavior, and
form/report UI generation.
From DevelopersGuide.adoc:473-494 and attributes.cljc:38-49:
;; When type is :ref, specify target(s)
(when (= :ref type)
(when-not (or (contains? m ::targets) (contains? m ::target))
(log/warn "Reference attribute" kw "does not list target(s)")))
Reference attributes represent edges in your data graph. They can be:
Cardinality indicates how many targets a reference can point to (from attributes-options.cljc:102-110):
ao/cardinality
"OPTIONAL. Default `:one`. Can also be `:many`.
This option indicates that this attribute either has a single value or a homogeneous set of values."
Cardinality: :one (default, can be omitted)
(defattr primary-address :account/primary-address :ref
{ao/target :address/id
ao/identities #{:account/id}
ao/schema :production})
;; ao/cardinality :one <-- implicit
An account has ONE primary address.
Checking in Code (attributes.cljc:100-104):
(attr/to-one? primary-address) ; => true
Cardinality: :many
(defattr addresses :account/addresses :ref
{ao/target :address/id
ao/cardinality :many
ao/identities #{:account/id}
ao/schema :production})
An account has MANY addresses.
Checking in Code (attributes.cljc:94-98):
(attr/to-many? addresses) ; => true
Most relationships point to a single entity type (from attributes-options.cljc:84-90):
ao/target
"REQUIRED for `:ref` attributes (unless you specify `ao/targets`). A qualified keyword
of an `identity? true` attribute that identifies the entities/rows/docs to which
this attribute refers."
Example:
(defattr invoice :line-item/invoice :ref
{ao/target :invoice/id
ao/identities #{:line-item/id}
ao/schema :production})
The ao/target value must be an identity attribute (one with ao/identity? true).
Added in v1.3.10+ (from attributes-options.cljc:92-100):
ao/targets
"ALTERNATIVE to `ao/target` for `:ref` attributes.
A SET of qualified keyword of an `identity? true` attribute that identifies the
entities/rows/docs to which this attribute can refer."
Example:
(defattr items :order/items :ref
{ao/targets #{:product/id :service/id} ; <-- SET
ao/cardinality :many
ao/identities #{:order/id}
ao/schema :production})
An order can contain products, services, or both.
EQL Generation (attributes.cljc:116-129):
(attr/attributes->eql [items-attr])
; With ao/target: [{:order/items [:product/id]}]
; With ao/targets: [{:order/items {:product/id [:product/id]
; :service/id [:service/id]}}]
From DevelopersGuide.adoc:1331-1345:
"One of the core questions in any relation is: does the referring entity/table/document 'own' the target? In other words does it create and destroy it?"
Concept: Parent exclusively owns children. When parent is deleted, children are deleted (cascade).
Indicator (from attributes-options.cljc:354-365):
ao/component?
"Used on `:ref` attributes. An indicator the reference edge points to entities that
are *exclusively owned* by the parent. A boolean or `(fn [owner] boolean?)`.
This *could* be used to:
* Generate schema auto-delete rules in database plugins.
* Check for dropped edges during save middleware to auto-delete orphans."
Example - Owned To-Many:
(defattr addresses :account/addresses :ref
{ao/target :address/id
ao/cardinality :many
ao/component? true ; <-- Parent owns these
ao/identities #{:account/id}
ao/schema :production})
Account owns its addresses. Deleting an account should delete its addresses.
Example - Owned To-One:
(defattr billing-info :account/billing-info :ref
{ao/target :billing/id
ao/component? true
ao/identities #{:account/id}
ao/schema :production})
Account owns its billing info. One-to-one ownership.
Concept: Target exists independently. Multiple entities can reference it.
Indicator: Omit ao/component? or set it to false.
Example - Referenced To-One:
(defattr item :line-item/item :ref
{ao/target :item/id
ao/identities #{:line-item/id}
ao/schema :production})
;; ao/component? false <-- implicit
Line items reference inventory items, but don't own them. Many line items can point to the same item.
Example - Referenced To-Many:
(defattr favorites :account/favorites :ref
{ao/target :product/id
ao/cardinality :many
ao/identities #{:account/id}
ao/schema :production})
Account favorites reference products. Products exist independently.
Use Case: Parent creates and owns a single child (e.g., Account owns billing info).
;; Parent (Account)
(defattr id :account/id :uuid
{ao/identity? true
ao/schema :production})
(defattr billing :account/billing :ref
{ao/target :billing/id
ao/component? true ; Owned
ao/identities #{:account/id}
ao/schema :production})
;; Child (Billing)
(defattr billing-id :billing/id :uuid
{ao/identity? true
ao/schema :production})
(defattr card-number :billing/card-number :string
{ao/identities #{:billing/id}
ao/schema :production})
Form Behavior: When editing an account, billing info appears as an embedded subform.
Use Case: Parent owns multiple children (e.g., Invoice owns line items).
From DevelopersGuide.adoc:1493-1547:
;; Parent (Invoice)
(defattr invoice-id :invoice/id :uuid
{ao/identity? true
ao/schema :production})
(defattr line-items :invoice/line-items :ref
{ao/target :line-item/id
ao/cardinality :many
ao/component? true ; Invoice owns line items
ao/identities #{:invoice/id}
ao/schema :production})
;; Child (LineItem)
(defattr line-item-id :line-item/id :uuid
{ao/identity? true
ao/schema :production})
(defattr quantity :line-item/quantity :int
{ao/identities #{:line-item/id}
ao/schema :production})
Form Behavior: Line items appear as a list of editable subforms with add/remove controls.
Form Configuration (DevelopersGuide.adoc:1527-1534):
(form/defsc-form InvoiceForm [this props]
{fo/id invoice/invoice-id
fo/attributes [invoice/line-items ...]
fo/subforms {:invoice/line-items {fo/ui LineItemForm
fo/can-add-row? (fn [parent] true)
fo/can-delete-row? (fn [parent item] true)}}})
Use Case: Child references a pre-existing parent (e.g., Line item references inventory item).
From DevelopersGuide.adoc:1357-1491:
;; Target (Inventory Item)
(defattr item-id :item/id :uuid
{ao/identity? true
ao/schema :production})
(defattr item-name :item/name :string
{ao/identities #{:item/id}
ao/schema :production})
;; Referrer (Line Item)
(defattr line-item-id :line-item/id :uuid
{ao/identity? true
ao/schema :production})
(defattr item :line-item/item :ref
{ao/target :item/id
;; NO ao/component? - not owned
ao/identities #{:line-item/id}
ao/schema :production})
Form Behavior: Use a picker/dropdown to select from existing items.
Form Configuration (DevelopersGuide.adoc:1441-1454):
(form/defsc-form LineItemForm [this props]
{fo/id line-item/line-item-id
fo/attributes [line-item/item ...]
fo/field-styles {:line-item/item :pick-one} ; <-- Picker style
fo/field-options {:line-item/item {::picker-options/query-key :item/all-items
::picker-options/options-xform (fn [_ raw]
(mapv
(fn [{:item/keys [id name]}]
{:text name :value [:item/id id]})
raw))}}})
The :pick-one style renders a dropdown/autocomplete instead of a subform.
Use Case: Parent references multiple pre-existing children (e.g., Account favorites).
;; Target (Product)
(defattr product-id :product/id :uuid
{ao/identity? true
ao/schema :production})
;; Referrer (Account)
(defattr account-id :account/id :uuid
{ao/identity? true
ao/schema :production})
(defattr favorites :account/favorites :ref
{ao/target :product/id
ao/cardinality :many
;; NO ao/component?
ao/identities #{:account/id}
ao/schema :production})
Form Behavior: Use a multi-select picker or tag-style UI.
Note: As of writing, this pattern requires custom UI (DevelopersGuide.adoc:1548-1550).
Use Case: Reference can point to multiple entity types.
;; Target 1 (Product)
(defattr product-id :product/id :uuid
{ao/identity? true
ao/schema :production})
;; Target 2 (Service)
(defattr service-id :service/id :uuid
{ao/identity? true
ao/schema :production})
;; Referrer (Order)
(defattr items :order/items :ref
{ao/targets #{:product/id :service/id} ; <-- Multiple targets
ao/cardinality :many
ao/identities #{:order/id}
ao/schema :production})
Database Adapter Note: Support varies. Some adapters may require additional configuration. Check your adapter docs.
RAD attributes are unidirectional by default. If you need bidirectional traversal, define attributes on both sides:
Example - Invoice ↔ Line Items:
;; Forward: Invoice -> Line Items
(defattr line-items :invoice/line-items :ref
{ao/target :line-item/id
ao/cardinality :many
ao/component? true
ao/identities #{:invoice/id}
ao/schema :production})
;; Reverse: Line Item -> Invoice
(defattr invoice :line-item/invoice :ref
{ao/target :invoice/id
ao/identities #{:line-item/id}
ao/schema :production})
Now you can navigate both directions:
{:invoice/line-items [:line-item/id ...]}{:line-item/invoice [:invoice/id ...]}Database Adapter Note: Some adapters (e.g., Datomic) can auto-generate reverse attributes. Check adapter docs.
| Pattern | Cardinality | Component? | Form Rendering | Example |
|---|---|---|---|---|
| Owned To-One | :one (default) | true | Embedded subform | Account → Billing Info |
| Owned To-Many | :many | true | List of subforms | Invoice → Line Items |
| Referenced To-One | :one (default) | false | Picker/Dropdown | Line Item → Inventory Item |
| Referenced To-Many | :many | false | Multi-select (custom) | Account → Favorites |
| Polymorphic | :many usually | varies | Custom or enhanced picker | Order → Products/Services |
Reference attributes work with database adapters to generate:
ao/component? true)Adapter-Specific Options: Database adapters add their own namespaced keys.
Example with Datomic (from DevelopersGuide.adoc:1505-1509):
(defattr addresses :account/addresses :ref
{ao/target :address/id
ao/cardinality :many
:com.fulcrologic.rad.database-adapters.datomic/schema :production
:com.fulcrologic.rad.database-adapters.datomic/entity-ids #{:account/id}})
The :com.fulcrologic.rad.database-adapters.datomic/* keys tell the Datomic adapter how to map this relationship.
See: 11-database-adapters.md for adapter-specific details.
When using owned relationships in forms, configure subforms with fo/subforms (from form-options namespace):
{fo/subforms {:attribute/name {fo/ui SubformComponent
fo/can-add-row? (fn [parent-props] boolean)
fo/can-delete-row? (fn [parent-props item-props] boolean)
fo/order-by (fn [items] sorted-items)}}}
fo/ui - REQUIRED. The form component for editing the target entity.
fo/ui AddressForm
fo/can-add-row? - (fn [parent-props] boolean-or-keyword). Controls add button visibility.
fo/can-add-row? (fn [account]
(< (count (:account/addresses account)) 5))
Return values:
true: Show add button, append new itemsfalse: Hide add button:prepend: Show add button, prepend new items:append: Show add button, append new items (same as true)fo/can-delete-row? - (fn [parent-props item-props] boolean). Controls delete button per item.
fo/can-delete-row? (fn [account address]
(not (:address/primary? address)))
fo/order-by - (fn [items] sorted-items). Custom sorting for subform items.
fo/order-by (fn [addresses]
(sort-by :address/created-at addresses))
From DevelopersGuide.adoc:1516-1534:
(ns com.example.ui.account-forms
(:require
[com.fulcrologic.rad.form :as form :refer [defsc-form]]
[com.fulcrologic.rad.form-options :as fo]
[com.example.model.account :as acct]
[com.example.model.address :as addr]
[com.example.ui.address-forms :refer [AddressForm]]))
;; Child form
(defsc-form AddressForm [this props]
{fo/id addr/id
fo/attributes [addr/street addr/city addr/state addr/zip]})
;; Parent form with subform
(defsc-form AccountForm [this props]
{fo/id acct/id
fo/attributes [acct/name acct/addresses]
fo/subforms {:account/addresses {fo/ui AddressForm
fo/can-add-row? (fn [acct] (< (count (:account/addresses acct)) 2))
fo/can-delete-row? (fn [acct addr] (< 1 (count (:account/addresses acct))))}}})
Behavior:
For referenced (non-owned) relationships, use pickers (from DevelopersGuide.adoc:1441-1479):
{fo/field-styles {:attribute/name :pick-one} ; or :pick-many
fo/field-options {:attribute/name {::picker-options/query-key ...
::picker-options/query-component ...
::picker-options/options-xform ...
::picker-options/cache-time-ms ...}}}
::picker-options/query-key - REQUIRED. Top-level EQL key that returns candidate entities.
::picker-options/query-key :item/all-items
Server must have a resolver:
(defattr all-items :item/all-items :ref
{ao/target :item/id
ao/pc-output [{:item/all-items [:item/id]}]
ao/pc-resolve (fn [env _]
#?(:clj {:item/all-items (db/get-all-items env)}))})
::picker-options/query-component - OPTIONAL. UI component for normalization.
::picker-options/query-component ItemForm
Allows picker options to be normalized into app database.
::picker-options/options-xform - (fn [normalized-result raw-result] options). Transforms results into picker
options.
::picker-options/options-xform (fn [normalized raw]
(mapv
(fn [{:item/keys [id name price]}]
{:text (str name " - $" price)
:value [:item/id id]})
(sort-by :item/name raw)))
Must return [{:text "..." :value ident-or-value} ...].
::picker-options/cache-key - OPTIONAL. Key for caching options (defaults to query-key).
::picker-options/cache-time-ms - OPTIONAL. Cache duration in milliseconds (default: 100ms).
::picker-options/cache-time-ms 60000 ; Cache for 1 minute
From DevelopersGuide.adoc:1406-1454:
(ns com.example.ui.line-item-forms
(:require
[com.fulcrologic.rad.form :as form :refer [defsc-form]]
[com.fulcrologic.rad.form-options :as fo]
[com.fulcrologic.rad.picker-options :as picker-options]
[com.example.model.line-item :as line-item]
[com.example.ui.item-forms :refer [ItemForm]]))
(defsc-form LineItemForm [this props]
{fo/id line-item/id
fo/attributes [line-item/item line-item/quantity]
;; item is a ref, but render as picker
fo/field-styles {:line-item/item :pick-one}
fo/field-options {:line-item/item
{::picker-options/query-key :item/all-items
::picker-options/query-component ItemForm
::picker-options/options-xform (fn [_ raw]
(mapv
(fn [{:item/keys [id name]}]
{:text name :value [:item/id id]})
(sort-by :item/name raw)))
::picker-options/cache-time-ms 60000}}})
Behavior: :line-item/item renders as a dropdown of inventory items instead of a subform.
Relationships don't have to be stored. Create virtual edges with custom resolvers (from DevelopersGuide.adoc:481-484):
;; Virtual to-one: Customer's most-shipped-to address
(defattr likely-address :customer/most-likely-address :ref
{ao/target :address/id
ao/pc-input #{:customer/id}
ao/pc-output [{:customer/most-likely-address [:address/id]}]
ao/pc-resolve (fn [env {:customer/keys [id]}]
#?(:clj
(let [addr-id (calc-most-likely-address env id)]
{:customer/most-likely-address [:address/id addr-id]})))})
This relationship:
ao/schema)Declare relationships on the referrer, not the target:
;; CORRECT: Account declares its relationship to Address
(ns com.example.model.account ...)
(defattr addresses :account/addresses :ref
{ao/target :address/id ...})
;; INCORRECT: Don't declare this on Address
(ns com.example.model.address ...)
(defattr account :address/account :ref ; <-- Only if you need reverse nav
{ao/target :account/id ...})
The ao/target (or ao/targets) must reference identity attributes:
;; Target
(defattr id :item/id :uuid
{ao/identity? true ...}) ; <-- MUST be identity
;; Reference
(defattr item :line-item/item :ref
{ao/target :item/id ...}) ; <-- Points to identity
Some database adapters support :many cardinality on scalar types (e.g., a person having multiple email addresses
stored as strings). Check your adapter docs (from attributes-options.cljc:108-109).
ao/component? true is a hint. Whether cascade deletes actually happen depends on:
Always test deletion behavior with your specific setup.
{ao/target :child/id
ao/cardinality :many
ao/component? true}
{ao/target :entity/id
;; omit ao/component?
fo/field-styles {:attr :pick-one}}
{ao/targets #{:type1/id :type2/id}
ao/cardinality :many}
;; Define both sides
{ao/target :other/id} ; forward
{ao/target :this/id} ; reverse (in other namespace)
fo/subforms, fo/can-add-row?, fo/can-delete-row?fo/field-stylesCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |