-
Notifications
You must be signed in to change notification settings - Fork 7
Description
Several different features have the same requirement of storing a list of values for individual entity IDs:
- networks
- infectiousness modifiers
- households, workplaces, schools
There are several ways to implement these. For example, we have a bespoke implementation for networks. But with entities we are now able to represent M-to-1 and M-to-N relationships with entities alone:
- M-to-1: If
EntityAinstance can have a singleEntityBinstance, butEntityB's can be associated with many distinctEntityA's, we just giveEntityAa new property to store theEntityBId. - M-to-N: If
EntityA's can be associated with manyEntityB's and vice versa, we create a third entity the instances of which represent association between individuals; call itFromEntityAToEntityB. It will have at least two properties: one for theEntityAIdand another for theEntityBId.
This raises the question of when it's actually better to do this than some alternative. So this issue is to figure that out.
Here is a user-guide level description of the technique. Should we make this an Ixa Book topic?
Modeling Many-to-Many Relationships with a Junction Entity
Introduction
Suppose you have two entities — Person and Modifier (a transmission modifier) — and you want to express that any person can be associated with multiple modifiers, and any modifier can be associated with multiple people. This is a classic many-to-many (M-to-N) relationship.
A way to model this in ixa is with a junction entity: a third entity whose instances each represent one link between a Person and a Modifier. We'll call it FromPersonToModifier.
Summary
The pattern is:
- Define your two base entities (
Person,Modifier). - Define a junction entity (
FromPersonToModifier). - Give the junction entity two required properties whose values are the
EntityIdtypes of the base entities (FromPerson(PersonId)andToModifier(ModifierId)). - Index those properties for efficient lookup in either direction.
This gives you a fully queryable, indexable M-to-N relationship, with the option to attach arbitrary metadata to each link.
Step 1: Define the two base entities
define_entity!(Person);
define_entity!(Modifier);This gives us the types Person, Modifier, and their ID aliases PersonId and ModifierId.
Step 2: Define the junction entity and its properties
define_entity!(FromPersonToModifier);Now we need two properties on FromPersonToModifier — one holding a PersonId and one holding a ModifierId. Since EntityId<E> already implements all the traits a property value needs (Copy, Clone, Debug, PartialEq, Serialize), we can use define_property! directly:
// A newtype wrapping PersonId, used as a property of FromPersonToModifier.
define_property!(struct FromPerson(PersonId), FromPersonToModifier);
// A newtype wrapping ModifierId, used as a property of FromPersonToModifier.
define_property!(struct ToModifier(ModifierId), FromPersonToModifier);Each FromPerson and ToModifier property is a simple tuple struct wrapping an entity ID. Because neither has a default_const, they are required — you must supply both when creating a junction entity instance.
Step 3: Create association instances
To associate a person with a modifier, you add a new FromPersonToModifier entity:
let person_id: PersonId = context.add_entity((Age(30),)).unwrap();
let modifier_id: ModifierId = context.add_entity((/* modifier properties */)).unwrap();
// Create the association
let _link_id: FromPersonToModifierId = context.add_entity((
FromPerson(person_id),
ToModifier(modifier_id),
)).unwrap();Because both properties are required (no defaults), every junction entity is guaranteed to reference both a person and a modifier.
Step 4: Query the relationship
To make queries efficient, index the properties you want to look up by:
context.index_property::<FromPersonToModifier, FromPerson>();
context.index_property::<FromPersonToModifier, ToModifier>();Now you can find all modifiers for a given person, or all people associated with a given modifier:
// Find all junction entities for a specific person
context.with_query_results((FromPerson(person_id),), &mut |results| {
for link_id in results {
let modifier: ToModifier = context.get_property(link_id);
// modifier.0 is the ModifierId
}
});
// Find all junction entities for a specific modifier
context.with_query_results((ToModifier(modifier_id),), &mut |results| {
for link_id in results {
let person: FromPerson = context.get_property(link_id);
// person.0 is the PersonId
}
});Step 5 (optional): Add properties to the relationship itself
Because the junction is a full entity, you can attach additional properties to it. For example, if you want to record the strength of a modifier's effect on a given person:
define_property!(struct EffectStrength(f64), FromPersonToModifier, default_const = EffectStrength(1.0));