Transactions

Biff uses XTDB for the database. It's OK if you haven't used XTDB before, but you may want to peruse some of the learning resources at least.

The system map (and by extension, incoming requests) includes a :biff.xtdb/node key which can be used to submit transactions:

(require '[xtdb.api :as xt])

(defn send-message [{:keys [biff.xtdb/node session params] :as ctx}]
  (xt/submit-tx node
    [[::xt/put {:xt/id (java.util.UUID/randomUUID)
                :msg/user (:uid session)
                :msg/text (:text params)
                :msg/sent-at (java.util.Date.)}]])
  ...)

Biff also provides a higher-level wrapper over xtdb.api/submit-tx. It lets you specify document types from your schema. If the document you're trying to write doesn't match its respective schema, the transaction will fail. In addition, Biff will call xt/await-tx on the result, so you can read your writes.

(require '[com.biffweb :as biff])

(defn send-message [{:keys [session params] :as ctx}]
  (biff/submit-tx
    ;; select-keys is for illustration. Normally you would just pass in ctx.
    (select-keys ctx [:biff.xtdb/node :biff/malli-opts])
    [{:db/doc-type :message
      :msg/user (:uid session)
      :msg/text (:text params)
      :msg/sent-at (java.util.Date.)}])
  ...)

If you don't set :xt/id, Biff will use (java.util.UUID/randomUUID) as the default value. The default operation is :xtdb.api/put.

Biff transactions can also include regular XT operations. Any element of the transaction that isn't a map will be assumed to be an XT operation:

(biff/submit-tx sys
  [{:db/doc-type :user
    :db/op :merge
    :xt/id (or (biff/lookup-id db :user/email email)
               (java.util.UUID/randomUUID))
    :user/email email}
   [::xt/fn :biff/ensure-unique {:user/email email}]])

If there is contention (e.g. if two concurrent requests attempt to write to update the same document, or if a transaction function fails), the transaction will be retried up to three times. If you pass a function that returns a transaction, then the function will be called again before each retry:

(biff/submit-tx ctx
  (fn [{:keys [biff/db]}]
    [{:db/doc-type :user
      :db/op :merge
      :xt/id (or (biff/lookup-id db :user/email email)
                 (java.util.UUID/randomUUID))
      :user/email email}
     [::xt/fn :biff/ensure-unique {:user/email email}]]))

As a convenience, any occurrences of :db/now will be replaced with (java.util.Date.):

(defn send-message [{:keys [session params] :as ctx}]
  (biff/submit-tx ctx
    [{:db/doc-type :message
      :msg/user (:uid session)
      :msg/text (:text params)
      :msg/sent-at :db/now}])
  ...)

Similarly, any keywords with a namespace of db.id will be replaced with random UUIDs:

(biff/submit-tx sys
  [{:db/doc-type :user
    :xt/id :db.id/bob
    :user/email "bob@example.com"}
   {:db/doc-type :msg
    :msg/user :db.id/bob
    :msg/text "I am bob"}
   {:db/doc-type :user
    :xt/id :db.id/alice
    :user/email "alice@example.com"}
   {:db/doc-type :msg
    :msg/user :db.id/alice
    :msg/text "I am not bob"}])

Document operations

You can delete a document by setting :db/op :delete:

(defn delete-message [{:keys [params] :as ctx}]
  (biff/submit-tx ctx
    [{:xt/id (java.util.UUID/fromString (:msg-id params))
      :db/op :delete}])
  ...)

If you set :db/op :update or :db/op :merge, the document will be merged into an existing document if it exists. The difference is that :update will cause the transaction to fail if the document doesn't already exist, while :merge will create the document.

(defn set-foo [{:keys [session params] :as ctx}]
  (biff/submit-tx ctx
    [{:db/op :update
      :db/doc-type :user
      :xt/id (:uid session)
      :user/foo (:foo params)}])
  ...)

There is also a :db/op :create operation which is the opposite of :db/op :update: the transaction will fail if the document does exist.

Biff uses :xtdb.api/match operations to ensure that concurrent merge/update operations don't get overwritten. If the match fails, the transaction will be retried up to three times.

You can use :db.op/upsert to update a document with the given attributes, or create a new one if doesn't exist yet:

(biff/submit-tx sys
  [{:db/doc-type :user
    :db.op/upsert {:user/email "hello@example.com"}
    :user/joined-at :db/now}])

If the document is created, :xt/id will be set to (random-uuid). A transaction function is used to make sure the operation is atomic. Besides that, upsert operations work the same as :db/op :merge operations.

Note: You must have installed the :biff/ensure-unique transaction function for this to work. This is done by default in new projects. See com.biffweb/tx-fns.

Attribute operations

Some operations can be used on a per-attribute basis. Often these operations use the attribute's previous value, along with new values you provide, to determine what the final value should be.

Use :db/union to coerce the previous value to a set and insert new values with clojure.set/union:

[{:db/op :update
  :db/doc-type :post
  :xt/id #uuid "..."
  :post/tags [:db/union "clojure" "almonds"]}]

Use :db/difference to do the opposite:

[{:db/op :update
  :db/doc-type :post
  :xt/id #uuid "..."
  :post/tags [:db/difference "almonds"]}]

Add to or subtract from numbers with :db/add:

[{:db/op :update
  :db/doc-type :account
  :xt/id #uuid "..."
  :account/balance [:db/add -50]}]

Use :db/default to set a value only if the existing document doesn't already contain the attribute:

[{:db/op :update
  :db/doc-type :user
  :xt/id #uuid "..."
  :user/favorite-color [:db/default :yellow]}]

Use :db/dissoc to remove an attribute:

[{:db/op :update
  :db/doc-type :user
  :xt/id #uuid "..."
  :user/foo :db/dissoc}]

Use :db/unique to ensure that no other document has the same value for the given attribute:

[{:db/doc-type :user
  :xt/id #uuid "..."
  :user/handle [:db/unique "hunter2"]}]

Note: You must have installed the :biff/ensure-unique transaction function for this to work. This is done by default in new projects. See com.biffweb/tx-fns.

:db/lookup

Note: :db/lookup is deprecated. It's recommended to use :db.op/upsert instead, as is done in the starter project.

Finally, you can use :db/lookup to enforce uniqueness constraints on attributes other than :xt/id:

[{:db/doc-type :user
  :xt/id [:db/lookup {:user/email "hello@example.com"}]}]

This will use a separate "lookup document" that, if the user has been created already, will look like this:

{:xt/id {:user/email "hello@example.com"}
 :db/owned-by ...}

where ... is a document ID. If the document doesn't exist, the ID will be (java.util.UUID/randomUUID), unless you pass in a different default ID with :db/lookup:

[{:db/doc-type :user
  :xt/id [:db/lookup {:user/email "hello@example.com"} #uuid "..."]}]

If the first value passed along with :db/lookup is a map, it will get merged in to the document. So our entire transaction would end up looking like this, assuming the user document doesn't already exist:

[{:db/doc-type :user
  :xt/id [:db/lookup {:user/email "hello@example.com"}]}]
;; =>
[[:xtdb.api/put {:xt/id #uuid "abc123"
                 :user/email "hello@example.com"}]
 [:xtdb.api/match {:user/email "hello@example.com"} nil]
 [:xtdb.api/put {:xt/id {:user/email "hello@example.com"}
                 :db/owned-by #uuid "abc123"}]]

See also:

Have a question? Join the #biff channel on Clojurians Slack, or ask on GitHub.

Sign up for Biff: The Newsletter
Announcements, blog posts, et cetera et cetera.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.