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.
Scaffold 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.
Define an entry type
Each entry has a type. This lets your application make sense of what would otherwise be a blob of arbitrary bytes. Our HDI library gives you macros to automatically define, serialize, and deserialize typed entries 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 Director(pub String);
#[hdk_entry_helper]
pub struct Movie {
pub title: String,
pub director_hash: EntryHash,
pub imdb_id: Option<String>,
pub release_date: Timestamp,
pub 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_types
macro on an enum of all the entry types:
use hdi::prelude::*;
#[hdk_entry_types]
// This macro is required by hdk_entry_types.
#[unit_enum(UnitEntryTypes)]
enum EntryTypes {
Director(Director),
Movie(Movie),
// other types...
}
This also gives you an enum that you can use later when you’re storing app data. 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.
Configure 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_types]
#[unit_enum(UnitEntryTypes)]
enum EntryTypes {
Director(Director),
#[entry_type(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_type(visibility = "private", )]
HomeMovie(Movie),
}
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_types
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::*;
// Import the entry types and the enum defined in the integrity zome.
use movie_integrity::*;
use chrono::DateTime;
let movie = Movie {
title: "The Good, the Bad, and the Ugly".to_string(),
director_hash: EntryHash::from_raw_36(vec![ /* hash of 'Sergio Leone' entry */ ]),
imdb_id: Some("tt0060196".to_string()),
release_date: Timestamp::from(
DateTime::parse_from_rfc3339("1966-12-23")?
.to_utc()
),
box_office_revenue: 389_000_000,
};
let create_action_hash = 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_types` 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),
)?;
Create with relaxed chain top ordering
If your entry doesn’t have any dependencies on other data, you can use relaxed chain top ordering to prevent possible transaction rollbacks (we’ll let that page explain when this could happen and how to design around it).
To use this feature, you’ll need to use the more low-level create
host function, which requires you to build a more complex input. This example batches updates to director entries, which don’t have to reference other data including each other, so they’re a good candidate for relaxed ordering.
use movie_integrity::{Director, EntryTypes};
use hdk::prelude::*;
let directors = vec![/* construct a vector of `Director` structs here */];
for director in directors.iter() {
// To specify chain top ordering other than the default Strict, we
// need to use the `create` host function which requires a bit more
// setup.
let entry = EntryTypes::Director(director);
let ScopedEntryDefIndex {
zome_index,
zome_type: entry_def_index,
} = (&entry).try_into()?;
let visibility = EntryVisibility::from(&entry);
let create_input = CreateInput::new(
EntryDefLocation::app(zome_index, entry_def_index),
visibility,
entry.try_into()?,
ChainTopOrdering::Relaxed,
);
create(create_input))?;
}
Create under the hood
When a zome function calls create
, Holochain does the following:
- 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 the pendingCreate
action to the calling zome function.
At this point, the action hasn’t been persisted to the source chain. Read the zome function call lifecycle section to find out more about persistence.
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 movie_integrity::*;
use chrono::DateTime;
let movie2 = Movie {
title: "The Good, the Bad, and the Ugly".to_string(),
director_hash: EntryHash::from_raw_36(vec![ /* hash of 'Sergio Leone' entry */ ]),
imdb_id: Some("tt0060196".to_string()),
release_date: Timestamp::from(
DateTime::parse_from_rfc3339("1966-12-23")?
.to_utc()
),
// Corrected from 389_000_000
box_office_revenue: 400_000_000,
};
let update_action_hash = update_entry(
create_action_hash,
&EntryTypes::Movie(movie2),
)?;
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 with relaxed chain top ordering
If you want to use relaxed chain top ordering, use the low-level update
instead:
use hdk::prelude::*;
use movie_integrity::*;
use chrono::DateTime;
// A simple struct to keep a mapping to an old director action hash to new
// entry content.
struct OldToNewDirector {
old_action_hash: ActionHash,
new_entry: Director,
}
let old_to_new_directors = vec![
/* construct a vector of old director action hashes and updated content */
];
for director in old_to_new_directors.iter() {
// To specify chain top ordering other than the default Strict, we
// need to use the `create` host function which requires a bit more
// setup.
let entry = EntryTypes::Director(&director.new_entry);
let ScopedEntryDefIndex {
zome_index,
zome_type: entry_def_index,
} = (&entry).try_into()?;
let visibility = EntryVisibility::from(&entry);
let update_input: UpdateInput = {
original_action_address: &director.old_action_hash,
entry: entry.try_into()?,
chain_top_ordering: ChainTopOrdering::Relaxed,
};
update(update_input)?;
}
Update under the hood
When a zome function calls create
, Holochain does the following:
- Build an entry creation action called
Update
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 and visibility are automatically retrieved from the original action.)
- Write the
Update
action and the serialized entry data to the scratch space. - Return the
ActionHash
of the pendingUpdate
action to the calling zome function.
As with Create
, the action hasn’t been persisted to the source chain yet. Read the zome function call lifecycle section to find out more about persistence.
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 = 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 deleted. An entry itself is only considered deleted when all entry creation actions that created it are marked deleted, and it can become live again in the future if a new entry creation action writes it. Deleted 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 with relaxed chain top ordering
To delete with relaxed chain top ordering, use the low-level delete
instead.
use hdk::prelude::*;
let actions_to_delete: Vec<ActionHash> = vec![/* construct vector here */];
for action in actions_to_delete.iter() {
let delete_input: DeleteInput = {
deletes_action_hash: action,
chain_top_ordering: ChainTopOrdering::Relaxed,
}
delete(delete_input)?;
}
Delete under the hood
Calling delete_entry
does the following:
- Write a
Delete
action to the scratch space. - Return the pending
ActionHash
of theDelete
action to the calling zome function.
As with Create
and Delete
, the action hasn’t been persisted to the source chain yet. Read the zome function call lifecycle section to find out more about persistence.
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
As a single record
Get a record by calling hdk::prelude::get
with the hash of either 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 = get(
action_hash,
// Get the data and metadata directly from the DHT, falling back to local
// storage if it can't access peers.
// You can also specify `GetOptions::local()`, which only accesses the local
// storage.
GetOptions::network()
)?;
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 = 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 = get_details(
action_hash,
GetOptions::network()
)?;
match maybe_details {
Some(Details::Record(record_details)) => {
let maybe_movie: Option<Movie> = record_details.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::network()
)?;
match maybe_details {
Some(Details::Entry(entry_details)) => {
let maybe_movie = entry_details.entry
.as_app_entry()
.map(|b| Movie::try_from(b.into_sb()))
.transpose()?;
match maybe_movie {
Some(movie) => debug!(
"Movie {} was written by {} agents, updated by {} agents, and deleted by {} agents.",
movie.title,
entry_details.actions.len(),
entry_details.updates.len(),
entry_details.deletes.len()
),
None => debug!("Movie entry couldn't be retrieved"),
}
}
_ => debug!("Movie entry not found"),
}
Community CRUD libraries
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_types
hdi::prelude::entry_def
hdk::prelude::create_entry
hdk::prelude::update_entry
hdi::prelude::delete_entry
Further reading
- Core Concepts: CRUD actions
- Build Guide: Identifiers
- Build Guide: Links, Paths, and Anchors
- CRDT.tech, a resource for learning about conflict-free replicated data types