Id
Status
This part of relm4-store
is rather set in stone. I don't expect any changes here.
What it is?
Id is a value which uniquely identifies the record. In our case it's about record in the store. If your store holds records from database, you can reuse the database id.
Application vs database
Let's talk a bit about records lifetime. If we would talk about the record from database point of view, you create a record by inserting all the data required and in most cases db somehow generates id for you. Every database I've worked with had some way to give unique id to the new record. So database doesn't need to deal with records which can't be identified by the key.
On the other hand your application first creates a record. Now you need to do two things at the same time
- Show the record to the user
- Persist record in the storage
If you show it before persisting then you send id which was generated by the database to all places where your record is shown so database and your application don't diverge. If you first persist and later show the record to the user it might lead to unresponsive application since user can be waiting for db instead of working.
Id structure
relm4-store-record
separates id into two concepts
- Memory representation
- Id state
Memory representation
relm4-store
gives you freedom how your id is structured in the memory. You can use usize
, or Uuid
, or i32
, or your custom structure. Memory representation must implement
Copy
PartialEq
Eq
std::hash::Hash
std::fmt::Debug
PartialEq
and Eq
PartialEq
and Eq
are required so we can tell wherever given one instance of the id is equal to other so we can distinguish if we talk about the different instance of the same record or not.
Example:
You would like to mark the task in the todo application as complete. You pull the record from the store. Update the complete field. Now you send it back to the store with new value. But store returned a copy of a task and not a mutable reference. Now inside of the store implementation we need to find the original task and replace it with a new value. To do so we use id.
Hash
Hash
is required because there is no order which can be defined on id and we somehow need to be able to create a mapping id -> record
and this gives us access to two excellent data structures HashMap
and HashSet
.
Copy
In general case record is many times larger then id. If we would keep ordered vector of records any shuffling operation would cause a massive amount of memory operations related to cloning records. On the other hand id is small and it's size should be perfectly known which makes memory copy super efficient operation.
Debug
If something goes wrong relm4-store
will report an issue using record id. Since id is unique it should give you enough information about which record caused an issue.
Implementing memory representation
To let relm4-store
what is memory representation of your id you must implement relm4-store-record::TemporaryIdAllocator
trait. It's defined as
pub trait TemporaryIdAllocator: Clone + Debug {
/// Type of values on which `Id` is based of
///
/// This type defines memory representation of the id for the record
type Type: Copy + PartialEq + Hash + Eq + std::fmt::Debug;
/// Returns value of new **temporary** id
///
/// Every call must return new different value otherwise it's possible to have a conflict which could end up
/// with data loss
fn new_id() -> Self::Type;
}
Type defines the memory layout of your id and all of the requirements mentioned before. Now lets go to the hard part, new_id
method. Why it's hard? In single execution of your application every time you call it it must return unique value. If you call it twice to different values. You call it from another thread it returns different value. Wherever you call it in your application it must return different value each time and to make it worse only state it has at it's disposal is global one. In C
there is rand()
function. There are a lot of similarities in expectations about how new_id
and rand
works.
Until now we've focused on why it's hard to write new_id
. Now let's talk about thinks that makes it easier for you. The most important part is, that it must return value which is unique only for given execution of the application. If you run your application twice it's not expected that values don't duplicate in such a case. It's expected that
it will return values proper for Id::New
. They might be illegal for Id::Permanent
.
Provided id allocators
This is a list of id allocators provided by the relm4-store
out of the box
Allocator name | Memory representation | Notes |
---|---|---|
UuidAllocator | uuid::Uuid | |
DefaultIdAllocator | uuid::Uuid | Type alias to UuidAllocator |
Uuid was chosen as default since it's easy to implement and provides much better guarantees on uniqueness of the id then required. UuidAllocator::new_id
returns random v4 uuid based on operating system RNG as the source of random bytes.
Why there is no allocator for i32, u32, i64, u64
I'll discuss it based on i32
example. All other allocators follows the same logic so if you replace i32
in this section with any other value arguments will still hold.
To return different values for each new_id
call we need to hold a counter somewhere in global scope. Let's use AtomicI32
value to do that. Now if we only use this allocator
once in our application that's fine. Since id's will be given proper values. If you use it for two kinds of records the new_id
requirements are still kept since they are unique in scope of two stores and not just one but is it valid in your application logic? Do you expect generated id's to keep an order? Or maybe you need them unique but random
so they are not predictable for security reasons?
There is a lot of decisions to make when you choose your temporary id allocator, so we've given up on implementing allocators requiring global state. In examples there is one which will show how to implement i32 id allocator.
Examples
In relm4-store-examples
crate you can find following examples showing custom memory representations for id's
Example name | Description |
---|---|
24_char_id_allocator | Shows how to implement an id which consist of 24 characters, both for new and permanent ids |
id_allocator_with_different_types_for_new_and_permanent | Shows how to implement an id which has different memory representation for new and permanent id's |
i32_allocator | Shows how to implement allocator returning consecutive i32 values |
Id state
This part is provided for you by the relm4-store
as relm4-store-record::Id
enum. It has two variants New
and Permanent
. New
is for records which has not been persisted. Permanent
is for records which were.
Definition of the relm4-store-record::Id
pub enum Id<T>
where
T: ?Sized + Record,
{
/// Id for records which were not committed yet to store
New{
/// Value of the id
value: <T::Allocator as TemporaryIdAllocator>::Type,
},
/// Id for records which are persisted already
///
/// What persisted means depends on the store.
Permanent {
/// Value of the id
value: <T::Allocator as TemporaryIdAllocator>::Type,
}
}
Persistance of the record is something which depends on the store/backend implementation. Some backends might consider persistance to be at the moment of inserting a data into the store. Some when remote server responded with acknowledgment of storing the data. Some when they serialize the content of the store to the hard disk. This transition might require some special attention from you when dealing with records.
Why so complex?
This whole machinery allows to reliably describe a state where records are not stored yet and they still retain uniqueness required by the rest of the application. This allows user to work with your software while application still waits for persisting a record. This also allows you to perform the switch to the new id generated by your storage without user ever being interrupted.