Declare relationships and properties
Properties and relationships make model semantics explicit and easy to query. Use this guide to choose stable readings, field names, and access patterns as you turn a model design into PyRel declarations.
- PyRel is installed and importable in Python. See Set Up Your Environment for instructions.
- You have declared concepts in a
Modelinstance. See Declare Concepts for instructions.
Understand relationships and properties
Section titled “Understand relationships and properties”The following sections give a high-level overview of what relationships and properties are, when to use each one, and naming conventions to use when you declare them.
What the components of a relationship are
Section titled “What the components of a relationship are”Relationships have the following components:
- A reading: a Python f-string that describes the relationship and its fields.
- One or more fields: “slots” in the reading that correspond to related concepts, defined by a
{Concept}or{Concept:field_name}placeholder in the reading f-string.
For example, here is a relationship with one unnamed field and one named field:
from relationalai.semantics import Model
m = Model("MyModel")
Customer = m.Concept("Customer")Order = m.Concept("Order")
# Declare a relationship between the Customer and Order conceptsCustomer.orders = m.Relationship(f"{Customer} places {Order:order}")- The relationship is declared with
Model.Relationshipand assigned toCustomer.orders. - The reading is
f"{Customer} places {Order:order}". - The fields are
Customer(unnamed) andOrder:order(namedorder).
What relationships do
Section titled “What relationships do”A relationship maps one or more input fields to an output field, where the reading describes the meaning of the relationship and the fields define how to access them in queries and definitions.
For example, the Customer.orders relationship declared above, and reproduced here for convenience, maps the Customer field (input) to the Order:order field (output):
# input field# \# vvvvvvvvCustomer.orders = m.Relationship(f"{Customer} places {Order:order}")# ^^^^^^^^^^^# /# output field- As the names “input” and “output” suggest, a relationship is a bit like a function. In queries and definitions, you call it with input arguments and to get a reference to the matching output values.
- Unlike a Python function, though, a relationship has no implementation code of its own.
What the difference is between properties and relationships
Section titled “What the difference is between properties and relationships”A relationship is a general association between entities that can have multiple fields and is declared with Model.Relationship.
A property is a specialized relationship that represents a single-valued attribute of an entity and is declared with Model.Property.
Use the following table to help you choose which one to use:
| What to use | When to use it |
|---|---|
Model.Property | Choose when the inputs uniquely determine one output value, like a person’s birth date or an order’s status. |
Model.Relationship | Choose when an input can have many outputs, or the association has multiple fields, like email addresses, memberships, or an order line with a product and quantity. |
How many fields a relationship can have
Section titled “How many fields a relationship can have”A relationship can have any number of fields. The following sections describe the most common shapes and when to use each one.
Binary relationships (2 fields): map one thing to another
Section titled “Binary relationships (2 fields): map one thing to another”A binary relationship has two fields and is the most common way to represent a simple association between two concepts, like “customer places order” or “person works at company”.
Unary relationships (1 field): Boolean flags
Section titled “Unary relationships (1 field): Boolean flags”A unary relationship has one field and represents a Boolean flag for a concept, like “Order is cancelled” or “Shipment is delayed”:
from relationalai.semantics import Model, String
m = Model("MyModel")Order = m.Concept("Order", identify_by={"id": String})
# Declare a unary relationship to flag cancelled ordersOrder.cancelled = m.Relationship(f"{Order} is cancelled")- The single field is an input field. There is no output field. Whether or not the flag is true for a given entity is determined by the presence or absence of the relationship.
- Unary relationships are preferable to Boolean-valued binary relationships (for example,
Order.cancelled = m.Relationship(f"{Order} has been cancelled {Boolean:status})) because they often use less space in the model and can be more performant to query.
N-ary relationships (3+ fields): include additional context
Section titled “N-ary relationships (3+ fields): include additional context”Choose an n-ary relationship when the association naturally has extra fields, like a timestamp for when a relationship began:
from relationalai.semantics import Model, String, DateTime
m = Model("MyModel")Product = m.Concept("Product", identify_by={"sku": String})
# Declare a relationship with three fieldsProduct.inventory = m.Relationship( f"{Product} on hand as of {DateTime:timestamp} is {Integer:quantity}")- The output field is always the last field in the reading, so some care must be taken when declaring n-ary relationships to ensure the reading is clear and the fields are easy to access.
- The more fields a relationship has, the more expensive it can be to query, so n-ary relationships should be used judiciously. Stick to three or four fields maximum, if possible.
Declare a property with Model.Property
Section titled “Declare a property with Model.Property”Declaring a property adds a single-valued attribute you can select, filter, and constrain.
Use this when an entity has an attribute that should have at most one value per entity (for example, age, status, or birth_date).
A property declaration does not load data by itself.
Use a reading f-string for a relationship
Section titled “Use a reading f-string for a relationship”Declare the property with Model.Property using a reading f-string:
from relationalai.semantics import Integer, Model
m = Model("MyModel")Person = m.Concept("Person")
Person.age = m.Property( f"{Person} is {Integer} years old", short_name="person_age", # Optional)Person.age = m.Property(...)declares a property and attaches it to thePersonconcept.- The reading string
f"{Person} is {Integer} years old"defines one input field (thePerson) and one output field (theInteger). - Because this is a property, the output field is single-valued for each input entity, so each person can have at most one age.
- The optional
short_name="person_age"argument registers this property with the key"person_age"in theModel.relationship_indexdictionary. If you don’t provide ashort_name, the property will not be accessible viaModel.relationship_index, but will still be viewable inModel.relationships.
(Advanced) Pass a list of Field objects for a relationship
Section titled “(Advanced) Pass a list of Field objects for a relationship”Declare the property with Model.Property by providing a list of Field objects instead of a reading string:
from relationalai.semantics import Integer, Modelfrom relationalai.semantics.frontend.base import Field
m = Model("MyModel")Person = m.Concept("Person")
Person.age = m.Property( fields=[Field("person", Person), Field("age", Integer)], short_name="person_age", # Optional)Person.age = m.Property(fields=[...])declares the property without parsing a reading string.Field("person", Person)defines an input field namedpersonwith typePerson.Field("age", Integer)defines the output field namedagewith typeInteger.- Because this is a property, the output field is single-valued for each input entity, so each person can have at most one age.
- The optional
short_name="person_age"argument registers this property with the key"person_age"in theModel.relationship_indexdictionary. If you don’t provide ashort_name, the property will not be accessible viaModel.relationship_index, but will still be viewable inModel.relationships.
- If you can express the property clearly as a short reading f-string, the reading-string version is usually easier to scan and maintain.
- The explicit
Fieldlist version is useful when you are generating fields programmatically or when the reading string would be too long or complex to be helpful. - PyRel does generate a default reading from the fields that includes internal type IDs, but it is usually less human-readable.
Declare a relationship with Model.Relationship
Section titled “Declare a relationship with Model.Relationship”Declaring a relationship adds a reusable association you can call like a function, select in queries, and traverse across concepts. Choose this for one-to-many and many-to-many associations, and for any association that naturally has multiple fields. Attaching the relationship to a concept attribute is usually easier to discover than keeping it as a standalone variable.
Use a reading f-string
Section titled “Use a reading f-string”Declare the relationship with Model.Relationship using a reading f-string:
from relationalai.semantics import Integer, Model, String
m = Model("MyModel")Person = m.Concept("Person", identify_by={"name": String})Company = m.Concept("Company", identify_by={"name": String})
Person.works_at = m.Relationship( f"{Person} works at {Company}", short_name="person_works_at", # Optional)- The relationship is declared with
Model.Relationshipand assigned toPerson.works_at. - The reading string
f"{Person} works at {Company}"defines an input field of typePersonand an output field of typeCompany. - Because this is a relationship, the output field can be multi-valued for each input entity, so a person can work at multiple companies.
- The optional
short_name="person_works_at"argument registers this relationship with the key"person_works_at"in theModel.relationship_indexdictionary. If you don’t provide ashort_name, the relationship will not be accessible viaModel.relationship_index, but will still be viewable inModel.relationships.
(Advanced) Pass a list of Field objects
Section titled “(Advanced) Pass a list of Field objects”Declare the relationship with Model.Relationship by providing a list of Field objects instead of a reading string:
from relationalai.semantics import Integer, Model, Stringfrom relationalai.semantics.frontend.base import Field
m = Model("MyModel")Person = m.Concept("Person", identify_by={"name": String})Company = m.Concept("Company", identify_by={"name": String})
Person.works_at = m.Relationship( fields=[Field("employee", Person), Field("company", Company)], short_name="person_works_at", # Optional)Person.works_at = m.Relationship(fields=[...])declares a relationship without parsing a reading string.Field("employee", Person)defines an input field namedemployeewith typePerson.Field("company", Company)defines the output field namedcompanywith typeCompany.- Because this is a relationship, the output field can be multi-valued for each input entity, so a person can work at multiple companies.
- The optional
short_name="person_works_at"argument registers this relationship with the key"person_works_at"in theModel.relationship_indexdictionary. If you don’t provide ashort_name, the relationship will not be accessible viaModel.relationship_index, but will still be viewable inModel.relationships.
- If you can express the relationship clearly as a short reading f-string, the reading-string version is usually easier to scan and maintain.
- The explicit
Fieldlist version is useful when you are generating fields programmatically or when the reading string would be too long or complex to be helpful. - If you omit
reading, PyRel generates a default reading from the fields that includes internal type IDs. It is usually less human-readable.
Declare an alternate reading for a relationship
Section titled “Declare an alternate reading for a relationship”An alternate reading gives the same underlying relationship a different human-readable phrase or direction. Use this when people naturally talk about the same association in multiple ways, for example by using the inverse of a binary relationship or using different phrasing in the reading.
Create an alternate (inverse) reading by calling Relationship.alt on a base relationship:
from relationalai.semantics import Model
m = Model("MyModel")Team = m.Concept("Team")Project = m.Concept("Project")
# Declare a base relationship with explicit field names.Team.projects = m.Relationship(f"{Team} works on {Project}")
# Add an alternate reading over the same underlying fields.Project.team = Team.projects.alt(f"{Project} is worked on by {Team}")Team.projectsdeclares the base relationship with stable field names (teamandproject).Team.projects.alt(...)creates an alternate reading that refers to the same underlying relationship and fields, but with different phrasing and direction. It is assigned toProject.teamto make it easy to discover when starting from theProjectconcept.- Both
Team.projectsandProject.teamcan be used interchangeably in queries and definitions, and they will return the same results because they refer to the same underlying relationship.
-
An alternate reading is another way to refer to the same underlying relationship. It does not create a new relationship or new fields.
-
Relationship.altmust use the same field concepts and field names as the base relationship. Reordering is OK, but changing names or types is not. For example, the following would raise an error because it changes the field names instead of just reordering them:# Fails because it changes the field names.Project.team = Team.works_on.alt(f"{Project:p} involves {Team:t}")
View all relationships declared in a model
Section titled “View all relationships declared in a model”Viewing relationships helps you debug what your model currently knows about, especially in notebooks and interactive exploration.
Choose Model.relationships when you want everything the model has created.
Choose Model.relationship_index when you want to look up a relationship by a stable name.
Use Model.relationships
Section titled “Use Model.relationships”To view all relationships and properties created in a model, inspect Model.relationships:
from relationalai.semantics import Integer, Model
m = Model("MyModel")Person = m.Concept("Person")Company = m.Concept("Company")
Person.age = m.Property(f"{Person} has {Integer:age}")Person.works_at = m.Relationship(f"{Person} works at {Company}")
# Print all relationships and properties declared in the modelprint(m.relationships)m.relationshipsis a list of every relationship/property object created viam.Relationshipandm.Property.- This is a good first stop when you are not sure what has been declared in the current model instance.
Use Model.relationship_index
Section titled “Use Model.relationship_index”To look up a relationship or property by short name, use Model.relationship_index:
from relationalai.semantics import Integer, Model
m = Model("MyModel")Person = m.Concept("Person")
Person.age = m.Property( f"{Person} has {Integer:age}", short_name="person_age", # Declare a short name to make it accessible in relationship_index)
# Use the short name to look up the relationship in the relationship_index dictionaryprint(m.relationship_index["person_age"])short_name="person_age"is the key used to register the property inm.relationship_index.m.relationship_index["person_age"]returns the underlyingRelationshiporPropertyobject associated with that short name.