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:

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:

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:

  1. 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.
  2. Write the Create action and the serialized entry data to the scratch space.
  3. Return the ActionHash of the pending Create 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:

  1. Build an entry creation action called Update that contains everything in a Create 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.)
  2. Write the Update action and the serialized entry data to the scratch space.
  3. Return the ActionHash of the pending Update 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.

Update 1
Create
Update 2
Update 3
Update 4

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).

Update 1
Create
Update 2
Update 3
Update 4

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:

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:

  1. Write a Delete action to the scratch space.
  2. Return the pending ActionHash of the Delete 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:

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"),
};

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

Further reading

It looks like you are using Internet Explorer. While the basic content is available, this is no longer a supported browser by the manufacturer, and no attention is being given to having IE work well here.