Entries
An entry is a blob of bytes that your application code gives meaning and structure to via serialization, deserialization, and validation.
Entries and actions
An entry is always paired with an entry creation action that tells you who authored it and when it was authored. Because of this, you don’t usually need to include author and timestamp fields in your entries. There are two kinds of entry creation action:
Create
, which creates a new piece of data in either the user’s private database or the application’s shared database, andUpdate
, which does the same asCreate
but also takes an existing piece of data and marks it as updated.
The pairing of an entry and the action that created it is called a record, which is the basic unit of data in a Holochain application.
Define an entry type
Each entry has a type, which your application code uses to make sense of the entry’s bytes. Our HDI library gives you macros to automatically define, serialize, and deserialize entry types to and from any Rust struct or enum that serde
can handle.
Entry types are defined in an integrity zome. To define an EntryType
, use the hdi::prelude::hdk_entry_helper
macro on your Rust type:
use hdi::prelude::*;
#[hdk_entry_helper]
pub struct Movie {
title: String,
director: String,
imdb_id: Option<String>,
release_date: Timestamp,
box_office_revenue: u128,
}
This implements a host of TryFrom
conversions that your type is expected to implement, along with serialization and deserialization functions.
In order to dispatch validation to the proper integrity zome, Holochain needs to know about all the entry types that your integrity zome defines. This is done by implementing a callback in your zome called entry_defs
, but it’s easier to use the hdi::prelude::hdk_entry_defs
macro on an enum of all the entry types:
use hdi::prelude::*;
#[hdk_entry_defs]
enum EntryTypes {
Movie(Movie),
// other types...
}
Configuring an entry type
Each variant in the enum should hold the Rust type that corresponds to it, and is implicitly marked with an entry_def
proc macro which, if you specify it explicitly, lets you configure the given entry type further:
- An entry type can be configured as private, which means that it’s never published to the DHT, but exists only on the author’s source chain. To do this, use the
visibility = "private"
argument. - A public entry type can be configured to expect a certain number of required validations, which is the number of validation receipts that an author tries to collect from authorities before they consider their entry published on the DHT. To do this, use the
required_validations = <num>
argument.
use hdi::prelude::*;
#[hdk_entry_defs]
enum EntryTypes {
#[entry_def(required_validations = 7, )]
Movie(Movie),
// You can reuse your Rust type in another entry type if you like. In this
// example, `HomeMovie` also (de)serializes to/from the `Movie` struct, but
// is actually a different entry type with different visibility, and can be
// subjected to different validation rules.
#[entry_def(visibility = "private", )]
HomeMovie(Movie),
}
This also gives you an enum that you can use later when you’re storing app data. This is important because, under the hood, an entry type consists of two bytes – an integrity zome index and an entry def index. These are required whenever you want to write an entry. Instead of having to remember those values every time you store something, your coordinator zome can just import and use this enum, which already knows how to convert each entry type to the right IDs.
Create an entry
Most of the time you’ll want to define your create, read, update, and delete (CRUD) functions in a coordinator zome rather than the integrity zome that defines it. This is because a coordinator zome is easier to update in the wild than an integrity zome.
Create an entry by calling hdk::prelude::create_entry
. If you used hdk_entry_helper
and hdk_entry_defs
macro in your integrity zome (see Define an entry type), you can use the entry types enum you defined, and the entry will be serialized and have the correct integrity zome and entry type indexes added to it.
use hdk::prelude::*;
use chrono::Date;
// Import the entry types and the enum defined in the integrity zome.
use movie_integrity::*;
let movie = Movie {
title: "The Good, the Bad, and the Ugly",
director: "Sergio Leone"
imdb_id: Some("tt0060196"),
release_date: Timestamp::from(Date::Utc("1966-12-23")),
box_office_revenue: 389_000_000,
};
let create_action_hash: ActionHash = create_entry(
// The value you pass to `create_entry` needs a lot of traits to tell
// Holochain which entry type from which integrity zome you're trying to
// create. The `hdk_entry_defs` macro will have set this up for you, so all
// you need to do is wrap your movie in the corresponding enum variant.
&EntryTypes::Movie(movie.clone()),
)?;
Create under the hood
When the client calls a zome function that calls create_entry
, Holochain does the following:
- Prepare a scratch space for making an atomic set of changes to the source chain for the agent’s cell.
- Build an entry creation action called
Create
that includes:- the author’s public key,
- a timestamp,
- the action’s sequence in the source chain and the previous action’s hash,
- the entry type (integrity zome index and entry type index), and
- the hash of the serialized entry data.
- Write the
Create
action and the serialized entry data to the scratch space. - Return the
ActionHash
of theCreate
action to the calling zome function. (At this point, the action hasn’t been persisted to the source chain.) - Wait for the zome function to complete.
- Convert the action to DHT operations.
- Run the validation callback for all DHT operations.
- If successful, continue.
- If unsuccessful, return the validation error to the client instead of the zome function’s return value.
- Compare the scratch space against the actual state of the source chain.
- If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a
HeadMoved
error is returned to the caller. - If the source chain has diverged and the write specified relaxed chain top ordering, the data in the scratch space is ‘rebased’ on top of the new source chain state as it’s being written.
- If the source chain has not diverged, the data in the scratch space is written to the source chain state.
- If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a
- Return the zome function’s return value to the client.
- In the background, publish all newly created DHT operations to their respective authority agents.
Update an entry
Update an entry creation action by calling hdk::prelude::update_entry
with the old action hash and the new entry data:
use hdk::prelude::*;
use chrono::Date;
use movie_integrity::*;
let movie2 = Movie {
title: "The Good, the Bad, and the Ugly",
director: "Sergio Leone"
imdb_id: Some("tt0060196"),
release_date: Timestamp::from(Date::Utc("1966-12-23")),
box_office_revenue: 400_000_000,
};
let update_action_hash: ActionHash = update_entry(
create_action_hash,
&EntryTypes::Movie(movie2.clone()),
)?;
An Update
action operates on an entry creation action (either a Create
or an Update
), not just an entry by itself. It also doesn’t remove the original data from the DHT; instead, it gets attached to both the original entry and its entry creation action. As an entry creation action itself, it references the hash of the new entry so it can be retrieved from the DHT.
Update under the hood
Calling update_entry
does the following:
- Prepare a scratch space for making an atomic set of changes to the source chain for the agent’s cell.
- Build an
Update
action that contains everything in aCreate
action, plus:- the hash of the original action and
- the hash of the original action’s serialized entry data. (Note that the entry type is automatically retrieved from the original action.)
- Write an
Update
action to the scratch space. - Return the
ActionHash
of theUpdate
action to the calling zome function. (At this point, the action hasn’t been persisted to the source chain.) - Wait for the zome function to complete.
- Convert the action to DHT operations.
- Run the validation callback for all DHT operations.
- If successful, continue.
- If unsuccessful, return the validation error to the client instead of the zome function’s return value.
- Compare the scratch space against the actual state of the source chain.
- If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a
HeadMoved
error is returned to the caller. - If the source chain has diverged and the write specified relaxed chain top ordering, the data in the scratch space is ‘rebased’ on top of the new source chain state as it’s being written.
- If the source chain has not diverged, the data in the scratch space is written to the source chain state.
- If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a
- Return the zome function’s return value to the client.
- In the background, publish all newly created DHT operations to their respective authority agents.
Update patterns
Holochain gives you this update_entry
function, but is somewhat unopinionated about how it’s used. While in most cases you’ll want to interpret it as applying to the original record (action + entry), there are cases where you might want to interpret it as applying to the original entry, because the Update
action is merely a piece of metadata attached to both, and can be retrieved along with the original data using hdk::prelude::get_details
(see below).
You can also choose what record updates should be attached to. You can structure them as a ‘list’, where all updates refer to the ActionHash
of the original Create
action.
Or you can structure your updates as a ‘chain’, where each update refers to the ActionHash
of the previous entry creation action (either an Update
or the original Create
).
If you structure your updates as a chain, you may want to also create links from the ActionHash
of the original Create
to each update in the chain, making it a hybrid of a list and a chain. This trades additional storage space for reduced lookup time.
Resolving update conflicts
It’s up to you to decide whether two updates on the same entry or action are a conflict. If your use case allows branching edits similar to Git, then conflicts aren’t an issue.
But if your use case needs a single canonical version of a resource, you’ll need to decide on a conflict resolution strategy to use at retrieval time.
If only the original author is permitted to update the entry, choosing the latest update is simple. Just choose the Update
action with the most recent timestamp, which is guaranteed to advance monotonically for any honest agent’s source chain. But if multiple agents are permitted to update an entry, it gets more complicated. Two agents could make an update at exactly the same time (or their action timestamps might be wrong or falsified). So, how do you decide which is the ‘latest’ update?
These are two common patterns:
- Use an opinionated, deterministic definition of ‘latest’ that can be calculated from the content of the update regardless of the writer.
- Model your updates with a data structure that can automatically merge simultaneous updates, such as a conflict-free replicated data type (CRDT).
Delete an entry
Delete an entry creation action by calling hdk::prelude::delete_entry
.
use hdk::prelude::*;
let delete_action_hash: ActionHash = delete_entry(
create_action_hash,
)?;
As with an update, this does not actually remove data from the source chain or the DHT. Instead, a Delete
action is authored, which attaches to the entry creation action and marks it as ‘dead’. An entry itself is only considered dead when all entry creation actions that created it are marked dead, and it can become live again in the future if a new entry creation action writes it. Dead data can still be retrieved with hdk::prelude::get_details
(see below).
In the future we plan to include a ‘purge’ functionality. This will give agents permission to actually erase an entry from their DHT store, but not its associated entry creation action.
Remember that, even once purge is implemented, it is impossible to force another person to delete data once they have seen it. Be deliberate about choosing what data becomes public in your app.
Delete under the hood
Calling delete_entry
does the following:
- Prepare a scratch space for making an atomic set of changes to the source chain for the agent’s cell.
- Write a
Delete
action to the scratch space. - Return the
ActionHash
of theDelete
action to the calling zome function. (At this point, the action hasn’t been persisted to the source chain.) - Wait for the zome function to complete.
- Convert the action to DHT operations.
- Run the validation callback for all DHT operations.
- If successful, continue.
- If unsuccessful, return the validation error to the client instead of the zome function’s return value.
- Compare the scratch space against the actual state of the source chain.
- If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a
HeadMoved
error is returned to the caller. - If the source chain has diverged and the write specified relaxed chain top ordering, the data in the scratch space is ‘rebased’ on top of the new source chain state as it’s being written.
- If the source chain has not diverged, the data in the scratch space is written to the source chain state.
- If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a
- Return the zome function’s return value to the client.
- In the background, publish all newly created DHT operations to their respective authority agents.
Identifiers on the DHT
Holochain uses the hash of a piece of content as its unique ID. In practice, different kinds of hashes have different meaning and suitability to use as an identifier:
- To identify the contents of an entry, use the entry’s
EntryHash
. Remember that, if two entry creation actions write identical entry contents, the entries will collide in the DHT. You may want this or you may not, depending on the nature of your entry type. - A common pattern to identify an instance of an entry (i.e., an entry authored by a specific agent at a specific time) is to use the
ActionHash
of its entry creation action instead. This gives you timestamp and authorship information for free, and can be a persistent way to identify the initial entry at the root of a tree of updates. - You can reference an agent via their
AgentPubKey
. This is a special type of DHT entry whose identifier is identical to its content — that is, the agent’s public key. You can use it just like anEntryHash
andActionHash
. - Finally, you can also use external identifiers (that is, IDs of data that’s not in the DHT) as long as they’re 32 bytes, which is useful for hashes and public keys. It’s up to you to determine how to handle these identifiers in your front end.
You can use any of these identifiers as a field in your entry types to model a many-to-one relationship, or you can use links between identifiers to model a one-to-many relationship.
Retrieve an entry
By record only
Get a record by calling hdk::prelude::get
with the hash of its entry creation action. The return value is a Result<holochain_integrity_types::record::Record>
.
You can also pass an entry hash to get
, and the record returned will contain the oldest live entry creation action that wrote it.
use hdk::prelude::*;
use movie_integrity::*;
let maybe_record: Option<Record> = get(
action_hash,
// Get the data and metadata directly from the DHT. You can also specify
// `GetOptions::content()`, which only accesses the DHT if the data at the
// supplied hash doesn't already exist locally.
GetOptions::latest()
)?;
match maybe_record {
Some(record) => {
// Not all types of action contain entry data, and if they do, it may
// not be accessible, so `.entry()` may return nothing. It may also be
// of an unexpected entry type, so it may not be deserializable to an
// instance of the expected Rust type. You can find out how to check for
// most of these edge cases by exploring the documentation for the
// `Record` type, but in this simple example we'll skip that and assume
// that the action hash does reference an action with entry data
// attached to it.
let maybe_movie: Option<Movie> = record.entry().to_app_option();
match maybe_movie {
Some(movie) => debug!(
"Movie {}, released {}, record stored by {} on {}",
movie.title,
movie.release_date,
record.action().author(),
record.action().timestamp()
),
None => debug!("Movie entry couldn't be retrieved"),
}
}
None => debug!("Movie record not found"),
}
All data, actions, and links at an address
Records
To get a record and all the updates, deletes, and outbound links associated with its action, as well as its current validation status, call hdk::prelude::get_details
with an action hash. You’ll receive a Result<holochain_zome_types::metadata::RecordDetails>
.
use hdk::prelude::*;
use movie_integrity::*;
let maybe_details: Option<Details> = get_details(
action_hash,
GetOptions::latest()
)?;
match maybe_details {
Some(Details::Record(record_details)) => {
let maybe_movie: Option<Movie> = record.entry().to_app_option();
match maybe_movie {
Some(movie) => debug!(
"Movie record {}, created on {}, was updated by {} agents and deleted by {} agents",
movie.title,
record_details.record.action().timestamp(),
record_details.updates.len(),
record_details.deletes.len()
),
None => debug!("Movie entry couldn't be retrieved"),
}
}
_ => debug!("Movie record not found"),
}
Entries
To get an entry and all the deletes and updates that operated on it (or rather, that operated on the entry creation actions that produced it), as well as all its entry creation actions and its current status on the DHT, pass an entry hash to hdk::prelude::get_details
. You’ll receive a holochain_zome_types::metadata::EntryDetails
struct.
use hdk::prelude::*;
use movie_integrity::*;
let maybe_details: Option<Details> = get_details(
entry_hash,
GetOptions::latest()
)?;
match maybe_details {
Some(Details::Entry(entry_details)) => {
let maybe_movie: Option<Movie> = entry_details.entry
.as_app_entry()
.clone()
.try_into()
.ok();
match maybe_movie {
Some(movie) => debug!(
"Movie {} was written by {} agents, updated by {} agents, and deleted by {} agents. Its DHT status is currently {}.",
movie.title,
entry_details.actions.len(),
entry_details.updates.len(),
entry_details.deletes.len(),
entry_details.entry_dht_status
),
None => debug!("Movie entry couldn't be retrieved"),
}
}
_ => debug!("Movie entry not found"),
}
Scaffolding an entry type and CRUD API
The Holochain dev tool command hc scaffold entry-type <entry_type>
generates the code for a simple entry type and a CRUD API. It presents an interface that lets you define a struct and its fields, then asks you to choose whether to implement update and delete functions for it along with the default create and read functions.
Community CRUD libraries
If the scaffolder doesn’t support your desired functionality, or is too low-level, there are some community-maintained libraries that offer opinionated and high-level ways to work with entries. Some of them also offer permissions management.
Reference
- hdi::prelude::hdk_entry_helper
- hdi::prelude::hdk_entry_defs
- hdi::prelude::entry_def
- hdk::prelude::create_entry
- hdk::prelude::update_entry
- hdi::prelude::delete_entry
Further reading
- Core Concepts: CRUD actions
- CRDT.tech, a resource for learning about conflict-free replicated data types