Table of Contents generated with DocToc
FOAM Relationships define typed links between models with automatic property, method, and DAO generation. They support two cardinalities:
The key benefit of FOAM relationships is that they eliminate manual junction table management. You declare the relationship once, and FOAM generates all the necessary models, properties, methods, and DAOs to navigate and manipulate the relationship from both sides.
FOAM provides two ways to link models: Reference properties and Relationships. Understanding when to use each is important.
A Reference is a simple foreign key - it stores the ID of another object and provides convenient access to it:
foam.CLASS({
package: 'com.foamdev.cook',
name: 'IngredientAmount',
properties: [
{ class: 'Long', name: 'id' },
{ class: 'Float', name: 'amount' },
{
class: 'Reference',
of: 'com.foamdev.cook.IngredientAmount',
name: 'alternative',
targetDAOKey: 'ingredientAmountDAO'
}
]
});
Use Reference when:
What Reference provides:
What Reference does NOT provide:
A Relationship creates bidirectional links with automatic property generation on both sides:
foam.RELATIONSHIP({
sourceModel: 'com.foamdev.cook.Ingredient',
targetModel: 'com.foamdev.cook.IngredientAmount',
forwardName: 'ingredientAmounts',
inverseName: 'ingredient',
cardinality: '1:*'
});
Use Relationship when:
| Feature | Reference | Relationship |
|---|---|---|
| Direction | One-way | Bidirectional |
| Code generated | A <name>$find accessor |
Properties + methods on both models |
| Collection access | Manual query | Automatic (source.targets returns DAO) |
| Inverse navigation | Not available | Automatic (target.source property) |
| Junction tables | Manual | Automatic for *:* |
| Self-reference | ✓ Supported | Not typical |
| UI integration | Picker/autocomplete | Table/browser + picker |
In a recipe application:
// Relationships.js - bidirectional links that are core to the domain
foam.RELATIONSHIP({
sourceModel: 'com.foamdev.cook.Recipe',
targetModel: 'com.foamdev.cook.RecipeStep',
forwardName: 'steps',
inverseName: 'recipe',
cardinality: '1:*'
});
foam.RELATIONSHIP({
sourceModel: 'com.foamdev.cook.RecipeStep',
targetModel: 'com.foamdev.cook.IngredientAmount',
forwardName: 'ingredientAmounts',
inverseName: 'recipeSteps',
cardinality: '*:*'
});
// IngredientAmount.js - simple self-reference for alternatives
{
class: 'Reference',
of: 'com.foamdev.cook.IngredientAmount',
name: 'alternative',
targetDAOKey: 'ingredientAmountDAO'
}
The alternative property is a Reference because:
The Recipe → RecipeStep link is a Relationship because:
recipe.steps to get all stepsstep.recipe to navigate backRelationships are declared using foam.RELATIONSHIP(), typically in the same file as one of the models:
foam.RELATIONSHIP({
sourceModel: 'com.example.Professor',
targetModel: 'com.example.Course',
forwardName: 'courses', // professor.courses
inverseName: 'professor', // course.professor
cardinality: '1:*' // default, can be omitted
});
| Property | Description |
|---|---|
sourceModel |
Fully qualified name of the “owning” model |
targetModel |
Fully qualified name of the “owned” or related model |
forwardName |
Property name added to source (e.g., professor.courses) |
inverseName |
Property name added to target (e.g., course.professor) |
cardinality |
'1:*' (default) or '*:*' |
sourceDAOKey |
DAO key for source model (defaults to sourceModel daoized) |
targetDAOKey |
DAO key for target model (defaults to targetModel daoized) |
junctionDAOKey |
For *:* only: DAO key for junction model |
sourceProperty |
Override configuration for the forward property |
targetProperty |
Override configuration for the inverse property |
oneWay |
If true, don’t install the inverse property on target |
When you define a 1:* relationship, FOAM automatically installs:
On the source model (e.g., Professor):
forwardName (e.g., courses)get<ForwardName>(x) that returns a RelationshipDAOOn the target model (e.g., Course):
inverseName (e.g., professor) that stores the source’s ID// Professor.js
foam.CLASS({
package: 'foam.core.demo.relationship',
name: 'Professor',
properties: [
{ class: 'String', name: 'id', hidden: true },
{ class: 'String', name: 'name' },
{ class: 'Float', name: 'salary' }
]
});
foam.RELATIONSHIP({
sourceModel: 'foam.core.demo.relationship.Professor',
targetModel: 'foam.core.demo.relationship.Course',
forwardName: 'courses',
inverseName: 'professor'
});
// Course.js
foam.CLASS({
package: 'foam.core.demo.relationship',
name: 'Course',
ids: [ 'code' ],
properties: [
{ class: 'String', name: 'code' },
{ class: 'String', name: 'title' },
{ class: 'Float', name: 'cost' }
]
});
After the relationship is processed, FOAM has effectively added:
// Generated on Professor (conceptually):
{
class: 'foam.dao.DAOProperty',
name: 'courses',
getter: function() {
return this.getCourses(this.__context__);
}
}
// Generated method on Professor:
function getCourses(x) {
return foam.dao.RelationshipDAO.create({
sourceId: this.id,
targetProperty: Course.PROFESSOR,
targetDAOKey: 'courseDAO'
}, x);
}
// Generated on Course:
{
class: 'Reference',
name: 'professor',
of: 'foam.core.demo.relationship.Professor',
targetDAOKey: 'professorDAO'
}
// Get all courses for a professor
var courses = await professor.courses.select();
console.log(courses.array);
// Add a new course to a professor (automatically sets course.professor)
var newCourse = Course.create({ code: 'CS101', title: 'Intro to CS' });
await professor.courses.put(newCourse);
// newCourse.professor is now set to professor.id
// Navigate from course back to professor
var prof = await professorDAO.find(course.professor);
// or: var prof = await course.professor$find;
The RelationshipDAO is a filtered view of the target DAO that:
put() through itFor *:* relationships, FOAM generates significantly more:
A Junction Model:
sourceId and targetId Reference properties<Source><Target>Junction by default (e.g., StudentCourseJunction)On both models:
ManyToManyRelationshipImplget<RelationshipName>(x)The ManyToManyRelationshipImpl provides:
dao - A filtered DAO showing related objectsjunctionDAO - Direct access to the junction recordsadd(target) - Create a relationshipremove(target) - Remove a relationship// Student.js
foam.CLASS({
package: 'foam.core.demo.relationship',
name: 'Student',
ids: [ 'studentId' ],
properties: [
{ class: 'String', name: 'name' },
{ class: 'Long', name: 'studentId' }
]
});
foam.RELATIONSHIP({
sourceModel: 'foam.core.demo.relationship.Student',
targetModel: 'foam.core.demo.relationship.Course',
cardinality: '*:*',
forwardName: 'courses',
inverseName: 'students'
});
FOAM automatically creates:
// Generated - you don't write this
foam.CLASS({
package: 'foam.core.demo.relationship',
name: 'StudentCourseJunction',
ids: ['sourceId', 'targetId'],
properties: [
{
class: 'Reference',
name: 'sourceId',
of: 'foam.core.demo.relationship.Student'
},
{
class: 'Reference',
name: 'targetId',
of: 'foam.core.demo.relationship.Course'
}
]
});
You need to provide a DAO for this junction in your services.jrl:
p({
"class": "foam.core.boot.CSpec",
"name": "studentCourseJunctionDAO",
"serve": true,
"serviceScript": """
return new foam.dao.EasyDAO.Builder(x)
.setOf(foam.core.demo.relationship.StudentCourseJunction.getOwnClassInfo())
.setJournalType(foam.dao.JournalType.SINGLE_JOURNAL)
.build();
"""
})
// Get all courses a student is enrolled in
var enrollment = student.courses; // Returns ManyToManyRelationshipImpl
var courses = await enrollment.dao.select();
console.log(courses.array);
// Enroll a student in a course
await student.courses.add(course);
// Unenroll a student from a course
await student.courses.remove(course);
// Get all students in a course (inverse direction)
var students = await course.students.dao.select();
// Add a student to a course (same effect as student.courses.add(course))
await course.students.add(student);
Without FOAM, managing many-to-many relationships requires:
// Traditional approach - lots of manual work
async function enrollStudent(studentId, courseId) {
// 1. Create junction record manually
await junctionDAO.put({ studentId, courseId });
}
async function getCoursesForStudent(studentId) {
// 2. Query junction table
var junctions = await junctionDAO
.where(EQ(Junction.STUDENT_ID, studentId))
.select();
// 3. Extract course IDs
var courseIds = junctions.array.map(j => j.courseId);
// 4. Query courses table
return courseDAO.where(IN(Course.ID, courseIds)).select();
}
async function getStudentsForCourse(courseId) {
// Same tedious process in reverse...
}
With FOAM relationships, all of this complexity is hidden:
// FOAM approach - simple and intuitive
await student.courses.add(course); // Enroll
await student.courses.remove(course); // Unenroll
var courses = await student.courses.dao.select(); // Get courses
var students = await course.students.dao.select(); // Get students
What FOAM handles automatically:
add()/remove()ManyToManyRelationshipDAOstudent.courses and course.students work)You can customize the generated properties:
foam.RELATIONSHIP({
sourceModel: 'com.example.Author',
targetModel: 'com.example.Book',
forwardName: 'books',
inverseName: 'author',
sourceProperty: {
label: 'Published Books',
visibility: 'RO'
},
targetProperty: {
label: 'Written By',
required: true
}
});
If your DAOs don’t follow the default naming convention:
foam.RELATIONSHIP({
sourceModel: 'com.example.Team',
targetModel: 'com.example.Player',
forwardName: 'roster',
inverseName: 'team',
sourceDAOKey: 'myTeamDAO',
targetDAOKey: 'myPlayerDAO'
});
Sometimes you only need navigation in one direction:
foam.RELATIONSHIP({
sourceModel: 'com.example.Order',
targetModel: 'com.example.AuditLog',
forwardName: 'auditLogs',
inverseName: 'order',
oneWay: true // Don't add 'order' property to AuditLog
});
FOAM’s UI layer automatically understands relationships:
ManyToManyRelationshipPropertyView providing Add/Remove actionsThe relationship properties respect visibility settings, so you can hide them on create forms and show them on detail views:
foam.RELATIONSHIP({
sourceModel: 'com.example.Project',
targetModel: 'com.example.Task',
forwardName: 'tasks',
inverseName: 'project',
sourceProperty: {
createVisibility: 'HIDDEN', // Don't show on create form
updateVisibility: 'RW' // Show when editing
}
});
Here’s a complete example showing models, relationships, services, and usage:
// Department.js
foam.CLASS({
package: 'com.example.university',
name: 'Department',
properties: [
{ class: 'Long', name: 'id' },
{ class: 'String', name: 'name', required: true },
{ class: 'String', name: 'building' }
]
});
foam.RELATIONSHIP({
sourceModel: 'com.example.university.Department',
targetModel: 'com.example.university.Professor',
forwardName: 'faculty',
inverseName: 'department'
});
// Professor.js
foam.CLASS({
package: 'com.example.university',
name: 'Professor',
properties: [
{ class: 'Long', name: 'id' },
{ class: 'String', name: 'name', required: true },
{ class: 'EMail', name: 'email' }
]
});
foam.RELATIONSHIP({
sourceModel: 'com.example.university.Professor',
targetModel: 'com.example.university.Course',
forwardName: 'courses',
inverseName: 'instructor'
});
// Course.js
foam.CLASS({
package: 'com.example.university',
name: 'Course',
properties: [
{ class: 'String', name: 'code' },
{ class: 'String', name: 'title', required: true },
{ class: 'Int', name: 'credits', min: 1, max: 6 }
],
ids: ['code']
});
// Student.js
foam.CLASS({
package: 'com.example.university',
name: 'Student',
properties: [
{ class: 'Long', name: 'id' },
{ class: 'String', name: 'name', required: true },
{ class: 'Int', name: 'year', min: 1, max: 4 }
]
});
foam.RELATIONSHIP({
sourceModel: 'com.example.university.Student',
targetModel: 'com.example.university.Course',
cardinality: '*:*',
forwardName: 'enrollments',
inverseName: 'students'
});
// services.jrl
p({
"class": "foam.core.boot.CSpec",
"name": "departmentDAO",
"serve": true,
"serviceScript": """
return new foam.dao.EasyDAO.Builder(x)
.setOf(com.example.university.Department.getOwnClassInfo())
.setSeqNo(true)
.setJournalType(foam.dao.JournalType.SINGLE_JOURNAL)
.build();
"""
})
p({
"class": "foam.core.boot.CSpec",
"name": "professorDAO",
"serve": true,
"serviceScript": """
return new foam.dao.EasyDAO.Builder(x)
.setOf(com.example.university.Professor.getOwnClassInfo())
.setSeqNo(true)
.setJournalType(foam.dao.JournalType.SINGLE_JOURNAL)
.build();
"""
})
p({
"class": "foam.core.boot.CSpec",
"name": "courseDAO",
"serve": true,
"serviceScript": """
return new foam.dao.EasyDAO.Builder(x)
.setOf(com.example.university.Course.getOwnClassInfo())
.setJournalType(foam.dao.JournalType.SINGLE_JOURNAL)
.build();
"""
})
p({
"class": "foam.core.boot.CSpec",
"name": "studentDAO",
"serve": true,
"serviceScript": """
return new foam.dao.EasyDAO.Builder(x)
.setOf(com.example.university.Student.getOwnClassInfo())
.setSeqNo(true)
.setJournalType(foam.dao.JournalType.SINGLE_JOURNAL)
.build();
"""
})
// Junction DAO for Student ↔ Course many-to-many
p({
"class": "foam.core.boot.CSpec",
"name": "studentCourseJunctionDAO",
"serve": true,
"serviceScript": """
return new foam.dao.EasyDAO.Builder(x)
.setOf(com.example.university.StudentCourseJunction.getOwnClassInfo())
.setJournalType(foam.dao.JournalType.SINGLE_JOURNAL)
.build();
"""
})
foam.CLASS({
name: 'UniversityService',
imports: [
'departmentDAO',
'professorDAO',
'courseDAO',
'studentDAO'
],
requires: [
'com.example.university.Department',
'com.example.university.Professor',
'com.example.university.Course',
'com.example.university.Student'
],
methods: [
async function setupUniversity() {
// Create department
var csDept = await this.departmentDAO.put(
this.Department.create({ name: 'Computer Science', building: 'Gates Hall' })
);
// Create professor in department (1:* relationship)
var prof = this.Professor.create({ name: 'Dr. Smith', email: 'smith@uni.edu' });
prof = await csDept.faculty.put(prof); // Automatically sets prof.department
// Create courses for professor (1:* relationship)
await prof.courses.put(this.Course.create({ code: 'CS101', title: 'Intro to Programming', credits: 3 }));
await prof.courses.put(this.Course.create({ code: 'CS201', title: 'Data Structures', credits: 4 }));
// Create students
var alice = await this.studentDAO.put(this.Student.create({ name: 'Alice', year: 2 }));
var bob = await this.studentDAO.put(this.Student.create({ name: 'Bob', year: 1 }));
// Enroll students in courses (*:* relationship)
var cs101 = await this.courseDAO.find('CS101');
var cs201 = await this.courseDAO.find('CS201');
await alice.enrollments.add(cs101);
await alice.enrollments.add(cs201);
await bob.enrollments.add(cs101);
// Query relationships
console.log('Courses in CS dept:');
var deptCourses = await csDept.faculty.select();
for ( var p of deptCourses.array ) {
var courses = await p.courses.select();
console.log(` ${p.name}: ${courses.array.map(c => c.code).join(', ')}`);
}
console.log('Students in CS101:');
var cs101Students = await cs101.students.dao.select();
console.log(` ${cs101Students.array.map(s => s.name).join(', ')}`);
console.log('Alice\'s courses:');
var aliceCourses = await alice.enrollments.dao.select();
console.log(` ${aliceCourses.array.map(c => c.code).join(', ')}`);
}
]
});
FOAM Relationships provide:
foam.RELATIONSHIP() call defines both sides*:*, junction management is completely hiddenThe key insight: relationships are first-class citizens in FOAM. Rather than manually managing foreign keys and junction tables, you declare the relationship once and let FOAM handle all the plumbing.