Modelización avanzada de estados

A primera vista, el diseño del almacenamiento clave-valor puede parecer difícil para quienes tienen experiencia en SQL. Aunque las bases de datos como MongoDB u otras bases de datos optimizadas utilizan almacenamiento de valores clave, sus bibliotecas ocultan la complejidad interna a los desarrolladores.

Es por eso que el sistema de almacenamiento en Cosmos-SDK puede no ser fácil de entender inicialmente. Sin embargo, una vez que se comprende el concepto, se vuelve sencillo.

Al implementar un modelo estatal, es importante dar un paso atrás y hacer algunas preguntas antes de comenzar la implementación. Por ejemplo:

  • ¿Realmente necesitas guardar esa información en el estado blockchain?

  • ¿Es realmente necesaria esa conexión? ¿Podría ser entregado a la interfaz de usuario por un recopilador de bases de datos fuera de la cadena?

Al hacer estas preguntas, puede evitar escribir datos innecesarios en el estado y utilizar un exceso de almacenamiento. Usar menos almacenamiento significa una ejecución más barata.

En este tutorial, creará un modelo de estado para el siguiente caso de negocio:

  • El sistema contendrá personas.

  • Las personas pueden convertirse en miembros de varios grupos.

  • Un grupo puede contener varios miembros.

  • Los miembros pueden tener roles en un grupo, como administrador, superadministrador, regular, etc.

Implementación ingenua

A continuación se muestra un diseño de relación cualquiera a cualquiera para guardar datos mediante ID. En primer lugar, los datos de la persona se indexan mediante un ID que se incrementa automáticamente:

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Person {
    pub name: String,
    pub age: i32,
    pub membership_ids: Vec<String>
}
pub const PEOPLE: Map<&[u8], Person> = Map::new("people");

En este diseño, los grupos también se indexan mediante un ID.

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Group {
    pub name: String,
    pub membership_ids: Vec<String>
}
pub const GROUPS: Map<&[u8], Group> = Map::new("groups");

Relación de grupo y persona establecida utilizando la estructura de membresía:

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Membership {
  pub person_id: String,
  pub group_id: String,
  pub membership_status_id: String
}
pub const MEMBERSHIPS: Map<&[u8], Membership> = Map::new("memberships");

Estado de membresía definido usando el campo String de estado.

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct MembershipStatus {
  pub status: String,
  pub membership_ids: Vec<String>
}
pub const MEMBERSHIP_STATUSES: Map<&[u8], MembershipStatus> = Map::new("membership_statuses");

Implementación optimizada

Usar una identificación para identificar a las personas puede parecer intuitivo, pero crea redundancia. Los ID son simplemente valores utilizados para identificar a un usuario, pero los usuarios ya están identificados por un valor único: su Address. Por lo tanto, es mejor indexar a las personas usando su Address, en lugar de números enteros incrementados automáticamente.

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Person {
    pub name: String,
    pub age: u8, // changed to u8 since ages are unsigned and 100 years max.
}
// Addr -> Person
pub const PEOPLE: Map<&[u8], Person> = Map::new("people");

Se eliminó member_id. Se cambió i32 a u8. La optimización de los tipos de variables mejora el consumo de gas, lo que se traduce en menos tarifas.

Ahora para el grupo:

Los grupos no tienen una dirección, por lo que tiene sentido identificarlos mediante ID de incremento automático. Si desea que los nombres de los grupos sean únicos, es mejor utilizar el nombre como índice.

pub const GROUP_COUNTER: Item<u64> = Item::new("group_counter");
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Group {
  pub name: String,
}
// u64 ID -> Group
pub const GROUPS: Map<U64Key, Group> = Map::new("groups");

Cuando se guarda un grupo, se requiere el ID de incremento automático y se guarda en el elemento GROUP_COUNTER. Para implementar esta lógica, es mejor ponerla bajo una función:


pub fn next_group_counter(store: &mut dyn Storage) -> StdResult<u64> {
  let id: u64 = GROUP_COUNTER.may_load(store)?.unwrap_or_default() + 1;
  GROUP_COUNTER.save(store, &id)?;
  Ok(id)
}
pub fn save_group(store: &mut dyn Storage, group: &Group) -> StdResult<()> {
  let id = next_group_counter(store)?;
  let key = U64Key::new(id);
  NEW_GROUPS.save(store, key, group)
}

Para establecer una relación entre grupos y personas, y definir el rol de una persona, sería necesario:

  • Listar usuarios bajo un grupo

  • Lista de grupos de una usuaria

Esto se puede lograr mediante la creación de índices secundarios. Le animamos a completar la implementación restante como ejercicio personal.

Last updated