One-to-Many Property Relationships
One-to-many property relationships model scenarios where entities have multiple attributes or properties that can't be efficiently stored as simple vertex properties. This pattern is ideal for user preferences, product specifications, contact information, and any flexible attribute system.
Pattern Overview
Key Characteristics
- Flexible Attributes: Support for dynamic property sets
- Typed Properties: Different property types (string, number, boolean, etc.)
- Versioning: Track property changes over time
- Validation: Property-specific validation rules
- Grouping: Organize properties into logical groups
Common Use Cases
- User preferences and settings
- Product attributes and specifications
- Contact information (multiple emails, phones)
- Configuration parameters
- Metadata and tags
- Custom fields and extensions
Schema Definition
Basic Property Schema
import { AgeSchemaClient } from 'age-schema-client';
const propertySchema = {
vertices: {
User: {
properties: {
id: { type: 'string', required: true },
username: { type: 'string', required: true },
email: { type: 'string', required: true },
created_at: { type: 'string' },
status: { type: 'string', default: 'active' }
},
required: ['id', 'username', 'email']
},
Property: {
properties: {
id: { type: 'string', required: true },
name: { type: 'string', required: true },
value: { type: 'string', required: true },
data_type: { type: 'string', required: true }, // 'string', 'number', 'boolean', 'json'
category: { type: 'string' },
description: { type: 'string' },
is_public: { type: 'boolean', default: false },
is_required: { type: 'boolean', default: false },
validation_rules: { type: 'string' }, // JSON validation rules
created_at: { type: 'string' },
updated_at: { type: 'string' }
},
required: ['id', 'name', 'value', 'data_type']
},
PropertyDefinition: {
properties: {
id: { type: 'string', required: true },
name: { type: 'string', required: true },
data_type: { type: 'string', required: true },
category: { type: 'string' },
description: { type: 'string' },
default_value: { type: 'string' },
validation_rules: { type: 'string' },
is_required: { type: 'boolean', default: false },
is_system: { type: 'boolean', default: false },
sort_order: { type: 'number' }
},
required: ['id', 'name', 'data_type']
},
Product: {
properties: {
id: { type: 'string', required: true },
name: { type: 'string', required: true },
sku: { type: 'string', required: true },
category: { type: 'string' },
price: { type: 'number' },
status: { type: 'string', default: 'active' }
},
required: ['id', 'name', 'sku']
}
},
edges: {
HAS_PROPERTY: {
from: 'User',
to: 'Property',
properties: {
created_at: { type: 'string' },
created_by: { type: 'string' },
is_inherited: { type: 'boolean', default: false },
source: { type: 'string' } // e.g., 'user_input', 'system', 'import'
}
},
HAS_ATTRIBUTE: {
from: 'Product',
to: 'Property',
properties: {
display_order: { type: 'number' },
is_searchable: { type: 'boolean', default: true },
is_filterable: { type: 'boolean', default: true },
created_at: { type: 'string' }
}
},
DEFINED_BY: {
from: 'Property',
to: 'PropertyDefinition',
properties: {
version: { type: 'string' },
created_at: { type: 'string' }
}
},
INHERITS_FROM: {
from: 'PropertyDefinition',
to: 'PropertyDefinition',
properties: {
inheritance_type: { type: 'string' }, // 'extends', 'overrides'
created_at: { type: 'string' }
}
}
}
};
Contact Information Schema
const contactSchema = {
vertices: {
Person: {
properties: {
id: { type: 'string', required: true },
first_name: { type: 'string', required: true },
last_name: { type: 'string', required: true },
title: { type: 'string' },
company: { type: 'string' }
},
required: ['id', 'first_name', 'last_name']
},
ContactInfo: {
properties: {
id: { type: 'string', required: true },
type: { type: 'string', required: true }, // 'email', 'phone', 'address', 'social'
value: { type: 'string', required: true },
label: { type: 'string' }, // 'home', 'work', 'mobile', 'primary'
is_primary: { type: 'boolean', default: false },
is_verified: { type: 'boolean', default: false },
country_code: { type: 'string' }, // for phone numbers
extension: { type: 'string' }, // for phone numbers
created_at: { type: 'string' },
verified_at: { type: 'string' }
},
required: ['id', 'type', 'value']
}
},
edges: {
HAS_CONTACT: {
from: 'Person',
to: 'ContactInfo',
properties: {
priority: { type: 'number' },
notes: { type: 'string' },
created_at: { type: 'string' },
last_used: { type: 'string' }
}
}
}
};
Bulk Loading Examples
User Preferences Data
const client = new AgeSchemaClient({
connectionString: 'postgresql://user:pass@localhost:5432/graphdb',
graphName: 'user_properties'
});
await client.loadSchema(propertySchema);
// User preferences and properties data
const userPropertiesData = {
vertices: [
// Users
{ label: 'User', properties: {
id: 'user-001', username: 'alice_smith', email: 'alice@example.com',
created_at: '2024-01-15T10:00:00Z', status: 'active'
}},
{ label: 'User', properties: {
id: 'user-002', username: 'bob_jones', email: 'bob@example.com',
created_at: '2024-01-20T14:30:00Z', status: 'active'
}},
// Property Definitions
{ label: 'PropertyDefinition', properties: {
id: 'prop-def-001', name: 'theme', data_type: 'string',
category: 'ui', description: 'UI theme preference',
default_value: 'light', validation_rules: '{"enum": ["light", "dark", "auto"]}',
sort_order: 1
}},
{ label: 'PropertyDefinition', properties: {
id: 'prop-def-002', name: 'language', data_type: 'string',
category: 'localization', description: 'Preferred language',
default_value: 'en', validation_rules: '{"pattern": "^[a-z]{2}$"}',
sort_order: 2
}},
{ label: 'PropertyDefinition', properties: {
id: 'prop-def-003', name: 'notifications_enabled', data_type: 'boolean',
category: 'notifications', description: 'Enable email notifications',
default_value: 'true', is_required: true, sort_order: 3
}},
{ label: 'PropertyDefinition', properties: {
id: 'prop-def-004', name: 'max_items_per_page', data_type: 'number',
category: 'ui', description: 'Items per page in lists',
default_value: '25', validation_rules: '{"minimum": 10, "maximum": 100}',
sort_order: 4
}},
// User Properties (actual values)
{ label: 'Property', properties: {
id: 'prop-001', name: 'theme', value: 'dark', data_type: 'string',
category: 'ui', is_public: false, created_at: '2024-01-15T10:05:00Z'
}},
{ label: 'Property', properties: {
id: 'prop-002', name: 'language', value: 'en', data_type: 'string',
category: 'localization', is_public: true, created_at: '2024-01-15T10:05:00Z'
}},
{ label: 'Property', properties: {
id: 'prop-003', name: 'notifications_enabled', value: 'true', data_type: 'boolean',
category: 'notifications', is_public: false, created_at: '2024-01-15T10:05:00Z'
}},
{ label: 'Property', properties: {
id: 'prop-004', name: 'max_items_per_page', value: '50', data_type: 'number',
category: 'ui', is_public: false, created_at: '2024-01-15T10:05:00Z'
}},
// Bob's properties (different values)
{ label: 'Property', properties: {
id: 'prop-005', name: 'theme', value: 'light', data_type: 'string',
category: 'ui', is_public: false, created_at: '2024-01-20T14:35:00Z'
}},
{ label: 'Property', properties: {
id: 'prop-006', name: 'language', value: 'es', data_type: 'string',
category: 'localization', is_public: true, created_at: '2024-01-20T14:35:00Z'
}},
{ label: 'Property', properties: {
id: 'prop-007', name: 'notifications_enabled', value: 'false', data_type: 'boolean',
category: 'notifications', is_public: false, created_at: '2024-01-20T14:35:00Z'
}}
],
edges: [
// Alice's properties
{ label: 'HAS_PROPERTY',
from: { label: 'User', properties: { id: 'user-001' } },
to: { label: 'Property', properties: { id: 'prop-001' } },
properties: { created_at: '2024-01-15T10:05:00Z', source: 'user_input' }
},
{ label: 'HAS_PROPERTY',
from: { label: 'User', properties: { id: 'user-001' } },
to: { label: 'Property', properties: { id: 'prop-002' } },
properties: { created_at: '2024-01-15T10:05:00Z', source: 'system' }
},
{ label: 'HAS_PROPERTY',
from: { label: 'User', properties: { id: 'user-001' } },
to: { label: 'Property', properties: { id: 'prop-003' } },
properties: { created_at: '2024-01-15T10:05:00Z', source: 'system' }
},
{ label: 'HAS_PROPERTY',
from: { label: 'User', properties: { id: 'user-001' } },
to: { label: 'Property', properties: { id: 'prop-004' } },
properties: { created_at: '2024-01-15T10:05:00Z', source: 'user_input' }
},
// Bob's properties
{ label: 'HAS_PROPERTY',
from: { label: 'User', properties: { id: 'user-002' } },
to: { label: 'Property', properties: { id: 'prop-005' } },
properties: { created_at: '2024-01-20T14:35:00Z', source: 'user_input' }
},
{ label: 'HAS_PROPERTY',
from: { label: 'User', properties: { id: 'user-002' } },
to: { label: 'Property', properties: { id: 'prop-006' } },
properties: { created_at: '2024-01-20T14:35:00Z', source: 'user_input' }
},
{ label: 'HAS_PROPERTY',
from: { label: 'User', properties: { id: 'user-002' } },
to: { label: 'Property', properties: { id: 'prop-007' } },
properties: { created_at: '2024-01-20T14:35:00Z', source: 'user_input' }
},
// Property definitions
{ label: 'DEFINED_BY',
from: { label: 'Property', properties: { id: 'prop-001' } },
to: { label: 'PropertyDefinition', properties: { id: 'prop-def-001' } },
properties: { version: '1.0', created_at: '2024-01-15T10:05:00Z' }
},
{ label: 'DEFINED_BY',
from: { label: 'Property', properties: { id: 'prop-002' } },
to: { label: 'PropertyDefinition', properties: { id: 'prop-def-002' } },
properties: { version: '1.0', created_at: '2024-01-15T10:05:00Z' }
},
{ label: 'DEFINED_BY',
from: { label: 'Property', properties: { id: 'prop-005' } },
to: { label: 'PropertyDefinition', properties: { id: 'prop-def-001' } },
properties: { version: '1.0', created_at: '2024-01-20T14:35:00Z' }
}
]
};
// Load the user properties data
const batchLoader = client.createBatchLoader();
await batchLoader.load(userPropertiesData);
Common Query Patterns
1. Get All User Properties
// Get all properties for a specific user
const userProperties = await client.query()
.match('User', 'user')
.where({ 'user.id': 'user-001' })
.match('user', 'HAS_PROPERTY', 'property')
.return([
'property.name',
'property.value',
'property.data_type',
'property.category'
])
.orderBy('property.category')
.execute();
2. Get Properties by Category
// Get UI-related properties for a user
const uiProperties = await client.query()
.match('User', 'user')
.where({ 'user.id': 'user-001' })
.match('user', 'HAS_PROPERTY', 'property')
.where({ 'property.category': 'ui' })
.return([
'property.name',
'property.value',
'property.data_type'
])
.execute();
3. Property Validation Query
// Get property with its definition for validation
const propertyWithDefinition = await client.query()
.match('User', 'user')
.where({ 'user.id': 'user-001' })
.match('user', 'HAS_PROPERTY', 'property')
.match('property', 'DEFINED_BY', 'definition')
.return([
'property.name',
'property.value',
'property.data_type',
'definition.validation_rules',
'definition.default_value'
])
.execute();
4. Find Users by Property Value
// Find all users with dark theme preference
const darkThemeUsers = await client.query()
.match('User', 'user')
.match('user', 'HAS_PROPERTY', 'property')
.where({
'property.name': 'theme',
'property.value': 'dark'
})
.return(['user.username', 'user.email'])
.execute();
5. Property Usage Statistics
// Get statistics on property usage
const propertyStats = await client.query()
.match('PropertyDefinition', 'definition')
.match('(property:Property)-[:DEFINED_BY]->(definition)')
.match('(user:User)-[:HAS_PROPERTY]->(property)')
.with('definition.name as property_name, collect(property.value) as values, count(user) as user_count')
.return([
'property_name',
'user_count',
'size(apoc.coll.toSet(values)) as unique_values',
'values'
])
.orderBy('user_count DESC')
.execute();
6. Contact Information Queries
// Get all contact information for a person
const contactInfo = await client.query()
.match('Person', 'person')
.where({ 'person.id': 'person-001' })
.match('person', 'HAS_CONTACT', 'contact', 'has_contact_rel')
.return([
'contact.type',
'contact.value',
'contact.label',
'contact.is_primary',
'has_contact_rel.priority'
])
.orderBy(['contact.type', 'has_contact_rel.priority'])
.execute();
7. Primary Contact Information
// Get primary contact information by type
const primaryContacts = await client.query()
.match('Person', 'person')
.where({ 'person.id': 'person-001' })
.match('person', 'HAS_CONTACT', 'contact')
.where({ 'contact.is_primary': true })
.return([
'contact.type',
'contact.value',
'contact.label'
])
.execute();
Performance Optimization
1. Property Indexing
// Create indexes for property queries
await client.executeSQL(`
CREATE INDEX IF NOT EXISTS idx_property_name_value
ON ag_catalog.property_vertex USING btree ((properties->>'name'), (properties->>'value'));
CREATE INDEX IF NOT EXISTS idx_property_category
ON ag_catalog.property_vertex USING btree ((properties->>'category'));
CREATE INDEX IF NOT EXISTS idx_contact_type_primary
ON ag_catalog.contactinfo_vertex USING btree ((properties->>'type'), ((properties->>'is_primary')::boolean));
`);
2. Property Caching Strategy
// Cache frequently accessed properties
const propertyCache = {
vertices: {
UserPropertyCache: {
properties: {
user_id: { type: 'string', required: true },
properties_json: { type: 'string', required: true }, // JSON blob of all properties
last_updated: { type: 'string', required: true },
cache_version: { type: 'string', default: '1.0' }
}
}
}
};
Best Practices
1. Property Definition Management
- Define property schemas before creating instances
- Use consistent naming conventions
- Implement proper validation rules
- Version property definitions
2. Data Type Handling
- Store all values as strings with type metadata
- Implement proper type conversion in application layer
- Validate data types before storage
- Handle null and undefined values consistently
3. Performance Considerations
- Index frequently queried properties
- Consider denormalization for read-heavy scenarios
- Use property categories for efficient filtering
- Implement caching for static properties
4. Schema Evolution
- Support property definition versioning
- Implement migration strategies for property changes
- Maintain backward compatibility
- Track property usage for cleanup
Next Steps
- Parent-Child Relationships - For hierarchical structures
- Product-Part Relationships - For component hierarchies
- Schema Validation - Property validation techniques