Skip to content

M-to-1 and M-to-N relationships between entities #761

@RobertJacobsonCDC

Description

@RobertJacobsonCDC

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:

  1. M-to-1: If EntityA instance can have a single EntityB instance, but EntityB's can be associated with many distinct EntityA's, we just give EntityA a new property to store the EntityBId.
  2. M-to-N: If EntityA's can be associated with many EntityB's and vice versa, we create a third entity the instances of which represent association between individuals; call it FromEntityAToEntityB. It will have at least two properties: one for the EntityAId and another for the EntityBId.

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:

  1. Define your two base entities (Person, Modifier).
  2. Define a junction entity (FromPersonToModifier).
  3. Give the junction entity two required properties whose values are the EntityId types of the base entities (FromPerson(PersonId) and ToModifier(ModifierId)).
  4. 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));

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions