Entity Lifecycle
Complete entity lifecycle from creation to deletion, including state management and relationships
Entity Lifecycle Management and Relationships
This comprehensive guide covers the complete lifecycle of entities in the Vyuh Entity System, from creation to deletion, including state management, relationships, and advanced manipulation APIs.
Entity Lifecycle Overview
Every entity in the system goes through distinct lifecycle stages:
Creation → Loading → Active Use → Updates → Archival/Deletion
↓ ↓ ↓ ↓ ↓
Events Events Events Events EventsLifecycle Stages
1. Entity Creation
Entities can be created through multiple pathways:
Factory Methods
class Equipment extends EntityBase {
// Direct constructor
Equipment({
required super.id,
required super.schemaType,
required this.name,
required this.serialNumber,
// ... other fields
});
// Factory for new entities
factory Equipment.create({
required String name,
required String serialNumber,
String? description,
}) {
return Equipment(
id: const Uuid().v4(),
schemaType: 'equipment',
name: name,
serialNumber: serialNumber,
description: description,
createdAt: DateTime.now(),
layout: 'default',
modifiers: const {},
);
}
// Factory from template
factory Equipment.fromTemplate(EquipmentTemplate template) {
return Equipment(
id: const Uuid().v4(),
schemaType: 'equipment',
name: template.name,
serialNumber: '', // To be assigned
category: template.category,
specifications: template.defaultSpecs,
createdAt: DateTime.now(),
);
}
}Builder Pattern
class EquipmentBuilder {
String? _name;
String? _serialNumber;
String? _locationId;
Map<String, dynamic> _specifications = {};
EquipmentBuilder withName(String name) {
_name = name;
return this;
}
EquipmentBuilder withSerialNumber(String serialNumber) {
_serialNumber = serialNumber;
return this;
}
EquipmentBuilder withLocation(String locationId) {
_locationId = locationId;
return this;
}
EquipmentBuilder withSpecification(String key, dynamic value) {
_specifications[key] = value;
return this;
}
Equipment build() {
if (_name == null || _serialNumber == null) {
throw StateError('Name and serial number are required');
}
return Equipment(
id: const Uuid().v4(),
schemaType: 'equipment',
name: _name!,
serialNumber: _serialNumber!,
locationId: _locationId,
specifications: _specifications,
createdAt: DateTime.now(),
);
}
}
// Usage
final equipment = EquipmentBuilder()
.withName('Centrifuge')
.withSerialNumber('CF-2024-001')
.withLocation('lab-1')
.withSpecification('maxRPM', 15000)
.withSpecification('capacity', '4x100ml')
.build();2. Entity Loading
Entities are loaded from various sources:
From JSON
// Standard deserialization
final equipment = Equipment.fromJson(jsonData);
// With validation
Equipment loadEquipmentWithValidation(Map<String, dynamic> json) {
// Validate required fields
if (!json.containsKey('serialNumber')) {
throw ValidationException('Serial number is required');
}
final equipment = Equipment.fromJson(json);
// Post-load validation
if (equipment.serialNumber.length < 5) {
throw ValidationException('Invalid serial number format');
}
return equipment;
}Lazy Loading
class LazyLoadedEquipment extends Equipment {
bool _specificationsLoaded = false;
Map<String, dynamic>? _cachedSpecifications;
@override
Map<String, dynamic> get specifications {
if (!_specificationsLoaded) {
_loadSpecifications();
}
return _cachedSpecifications ?? {};
}
Future<void> _loadSpecifications() async {
_cachedSpecifications = await EquipmentApi.loadSpecifications(id);
_specificationsLoaded = true;
}
}3. Active Entity State Management
During active use, entities maintain state and handle changes:
State Tracking
mixin EntityStateTracking on EntityBase {
final Set<String> _modifiedFields = {};
Map<String, dynamic>? _originalValues;
void trackChange(String field, dynamic oldValue, dynamic newValue) {
if (_originalValues == null) {
_originalValues = {};
}
if (!_originalValues!.containsKey(field)) {
_originalValues![field] = oldValue;
}
if (_originalValues![field] == newValue) {
_modifiedFields.remove(field);
} else {
_modifiedFields.add(field);
}
}
bool get hasChanges => _modifiedFields.isNotEmpty;
Set<String> get modifiedFields => Set.unmodifiable(_modifiedFields);
void resetChanges() {
_modifiedFields.clear();
_originalValues = null;
}
}
// Usage in entity
class TrackedUser extends User with EntityStateTracking {
String _email;
@override
String get email => _email;
set email(String value) {
if (_email != value) {
trackChange('email', _email, value);
_email = value;
}
}
}Observable Entities
class ObservableEntity<T extends EntityBase> extends ChangeNotifier {
T _entity;
ObservableEntity(this._entity);
T get entity => _entity;
void update(T Function(T) updater) {
_entity = updater(_entity);
notifyListeners();
}
void updateField<V>(
V Function(T) getter,
T Function(T, V) setter,
V newValue,
) {
final currentValue = getter(_entity);
if (currentValue != newValue) {
_entity = setter(_entity, newValue);
notifyListeners();
}
}
}
// Usage
final observableUser = ObservableEntity(user);
observableUser.updateField(
(u) => u.email,
(u, email) => u.copyWith(email: email),
'[email protected]',
);4. Entity Updates
Entities support various update patterns:
Immutable Updates
extension EquipmentUpdates on Equipment {
Equipment copyWith({
String? name,
String? serialNumber,
String? locationId,
EquipmentStatus? status,
Map<String, dynamic>? specifications,
}) {
return Equipment(
id: id,
schemaType: schemaType,
name: name ?? this.name,
serialNumber: serialNumber ?? this.serialNumber,
locationId: locationId ?? this.locationId,
status: status ?? this.status,
specifications: specifications ?? this.specifications,
createdAt: createdAt,
modifiers: modifiers,
);
}
}Partial Updates
class PartialUpdate<T extends EntityBase> {
final String entityId;
final Map<String, dynamic> updates;
final Set<String> removedFields;
PartialUpdate({
required this.entityId,
required this.updates,
this.removedFields = const {},
});
T apply(T entity) {
final json = entity.toJson();
// Apply updates
updates.forEach((key, value) {
json[key] = value;
});
// Remove fields
for (final field in removedFields) {
json.remove(field);
}
return entity.constructor.fromJson(json);
}
}Batch Updates
class BatchEntityUpdater<T extends EntityBase> {
final List<T> entities;
final EntityApi<T> api;
BatchEntityUpdater({
required this.entities,
required this.api,
});
Future<List<T>> updateAll(T Function(T) updater) async {
final updates = <String, T>{};
for (final entity in entities) {
final updated = updater(entity);
if (updated != entity) {
updates[entity.id] = updated;
}
}
if (updates.isEmpty) return entities;
// Batch API call
final response = await api.batchUpdate(updates);
return response;
}
Future<List<T>> updateWhere(
bool Function(T) predicate,
T Function(T) updater,
) async {
final toUpdate = entities.where(predicate).toList();
return updateAll((entity) {
return toUpdate.contains(entity) ? updater(entity) : entity;
});
}
}5. Entity Deletion
Entities can be deleted with different strategies:
Soft Delete
mixin SoftDeletable on EntityBase {
bool get isDeleted => modifiers['deleted'] == true;
DateTime? get deletedAt => modifiers['deletedAt'] != null
? DateTime.parse(modifiers['deletedAt'])
: null;
String? get deletedBy => modifiers['deletedBy'];
Map<String, dynamic> softDelete({required String userId}) {
return {
...toJson(),
'modifiers': {
...modifiers,
'deleted': true,
'deletedAt': DateTime.now().toIso8601String(),
'deletedBy': userId,
},
};
}
Map<String, dynamic> restore() {
final updated = Map<String, dynamic>.from(modifiers);
updated.remove('deleted');
updated.remove('deletedAt');
updated.remove('deletedBy');
return {
...toJson(),
'modifiers': updated,
};
}
}Cascade Delete
class CascadeDeleteService {
Future<void> deleteWithRelated(
EntityBase entity,
List<RelationshipDefinition> relationships,
) async {
// Start transaction
await database.transaction((tx) async {
// Delete related entities first
for (final relationship in relationships) {
if (relationship.onDelete == OnDeleteAction.cascade) {
await tx.delete(
relationship.relatedTable,
where: '${relationship.foreignKey} = ?',
whereArgs: [entity.id],
);
}
}
// Delete main entity
await tx.delete(
entity.schemaType,
where: 'id = ?',
whereArgs: [entity.id],
);
});
}
}Entity Relationships
Defining Relationships
class RelationshipDefinition {
final String name;
final String relatedEntity;
final RelationType type;
final String? foreignKey;
final String? inverseForeignKey;
final OnDeleteAction onDelete;
final OnUpdateAction onUpdate;
const RelationshipDefinition({
required this.name,
required this.relatedEntity,
required this.type,
this.foreignKey,
this.inverseForeignKey,
this.onDelete = OnDeleteAction.restrict,
this.onUpdate = OnUpdateAction.cascade,
});
}
enum RelationType {
oneToOne,
oneToMany,
manyToOne,
manyToMany,
}
enum OnDeleteAction {
cascade,
setNull,
restrict,
}One-to-Many Relationships
class Location extends EntityBase {
// Location has many equipment
Future<List<Equipment>> get equipment async {
return await EquipmentApi().list(
filters: {'locationId': id},
);
}
}
class Equipment extends EntityBase {
final String? locationId;
// Equipment belongs to location
Future<Location?> get location async {
if (locationId == null) return null;
return await LocationApi().byId(locationId!);
}
}Many-to-Many Relationships
class User extends EntityBase {
// User has many roles through user_roles
Future<List<Role>> get roles async {
final userRoles = await database.query(
'user_roles',
where: 'user_id = ?',
whereArgs: [id],
);
final roleIds = userRoles.map((ur) => ur['role_id'] as String).toList();
return await RoleApi().getByIds(roleIds);
}
Future<void> addRole(String roleId) async {
await database.insert('user_roles', {
'user_id': id,
'role_id': roleId,
'assigned_at': DateTime.now().toIso8601String(),
});
}
Future<void> removeRole(String roleId) async {
await database.delete(
'user_roles',
where: 'user_id = ? AND role_id = ?',
whereArgs: [id, roleId],
);
}
}Eager Loading
class EntityLoader<T extends EntityBase> {
final EntityApi<T> api;
final List<RelationshipDefinition> relationships;
EntityLoader({
required this.api,
required this.relationships,
});
Future<T> loadWithRelationships(String id) async {
final entity = await api.byId(id);
if (entity == null) throw NotFoundException();
final json = entity.toJson();
// Load all relationships
for (final rel in relationships) {
switch (rel.type) {
case RelationType.oneToMany:
json[rel.name] = await _loadOneToMany(entity, rel);
break;
case RelationType.manyToOne:
json[rel.name] = await _loadManyToOne(entity, rel);
break;
case RelationType.manyToMany:
json[rel.name] = await _loadManyToMany(entity, rel);
break;
case RelationType.oneToOne:
json[rel.name] = await _loadOneToOne(entity, rel);
break;
}
}
return api.fromJson(json);
}
Future<List<dynamic>> _loadOneToMany(
T entity,
RelationshipDefinition rel,
) async {
final relatedApi = EntityRegistry.getApi(rel.relatedEntity);
return await relatedApi.list(
filters: {rel.foreignKey ?? '${entity.schemaType}_id': entity.id},
);
}
}Advanced Lifecycle Features
Entity Versioning
mixin Versionable on EntityBase {
int get version => modifiers['version'] ?? 1;
Map<String, dynamic> incrementVersion() {
return {
...toJson(),
'modifiers': {
...modifiers,
'version': version + 1,
'versionedAt': DateTime.now().toIso8601String(),
},
};
}
}
class VersionedEntity<T extends EntityBase> {
final T current;
final List<T> history;
VersionedEntity({
required this.current,
required this.history,
});
T? getVersion(int version) {
return history.firstWhereOrNull((e) => e.version == version);
}
List<EntityDiff> getDiffs() {
final diffs = <EntityDiff>[];
for (var i = 1; i < history.length; i++) {
diffs.add(EntityDiff.between(history[i - 1], history[i]));
}
return diffs;
}
}Entity Cloning
extension EntityCloning<T extends EntityBase> on T {
T clone({
String? newId,
Map<String, dynamic>? overrides,
bool resetTimestamps = true,
}) {
final json = toJson();
// Update ID
json['id'] = newId ?? const Uuid().v4();
// Reset timestamps
if (resetTimestamps) {
json['createdAt'] = DateTime.now().toIso8601String();
json.remove('updatedAt');
}
// Apply overrides
if (overrides != null) {
json.addAll(overrides);
}
// Clear version info
final modifiers = Map<String, dynamic>.from(json['modifiers'] ?? {});
modifiers.remove('version');
modifiers.remove('versionedAt');
json['modifiers'] = modifiers;
return (this as dynamic).fromJson(json) as T;
}
T deepClone({
required Map<String, RelationshipDefinition> relationships,
}) {
final cloned = clone();
// Clone related entities
for (final entry in relationships.entries) {
final rel = entry.value;
if (rel.type == RelationType.oneToMany) {
// Clone child entities
// Implementation depends on your specific needs
}
}
return cloned;
}
}Import/Export
class EntityExporter<T extends EntityBase> {
final EntityApi<T> api;
EntityExporter({required this.api});
Future<String> exportToJson(List<String> ids) async {
final entities = await Future.wait(
ids.map((id) => api.byId(id)),
);
final validEntities = entities.whereType<T>().toList();
return jsonEncode({
'exported_at': DateTime.now().toIso8601String(),
'entity_type': T.toString(),
'count': validEntities.length,
'entities': validEntities.map((e) => e.toJson()).toList(),
});
}
Future<String> exportToCsv(
List<String> ids,
List<String> fields,
) async {
final entities = await Future.wait(
ids.map((id) => api.byId(id)),
);
final csv = StringBuffer();
// Header
csv.writeln(fields.join(','));
// Data
for (final entity in entities.whereType<T>()) {
final json = entity.toJson();
final values = fields.map((field) {
final value = json[field];
// Escape CSV values
if (value.toString().contains(',')) {
return '"${value.toString().replaceAll('"', '""')}"';
}
return value.toString();
});
csv.writeln(values.join(','));
}
return csv.toString();
}
}
class EntityImporter<T extends EntityBase> {
final EntityApi<T> api;
EntityImporter({required this.api});
Future<List<T>> importFromJson(String jsonData) async {
final data = jsonDecode(jsonData);
final entities = (data['entities'] as List)
.map((json) => api.fromJson(json))
.toList();
// Validate before import
for (final entity in entities) {
await validateEntity(entity);
}
// Import with new IDs
final imported = <T>[];
for (final entity in entities) {
final newEntity = entity.clone();
final created = await api.createNew(newEntity);
imported.add(created);
}
return imported;
}
}Lifecycle Hooks
abstract class EntityLifecycleHooks<T extends EntityBase> {
Future<void> beforeCreate(T entity) async {}
Future<void> afterCreate(T entity) async {}
Future<void> beforeUpdate(T oldEntity, T newEntity) async {}
Future<void> afterUpdate(T oldEntity, T newEntity) async {}
Future<void> beforeDelete(T entity) async {}
Future<void> afterDelete(T entity) async {}
}
class EquipmentLifecycleHooks extends EntityLifecycleHooks<Equipment> {
@override
Future<void> beforeCreate(Equipment entity) async {
// Validate serial number uniqueness
final existing = await EquipmentApi().findBySerialNumber(
entity.serialNumber,
);
if (existing != null) {
throw ValidationException('Serial number already exists');
}
}
@override
Future<void> afterCreate(Equipment entity) async {
// Create initial calibration record
await CalibrationApi().createInitialRecord(entity.id);
// Send notification
await NotificationService.notify(
'New equipment added: ${entity.name}',
);
}
@override
Future<void> beforeDelete(Equipment entity) async {
// Check if equipment is in use
final inUse = await EquipmentUsageApi().isInUse(entity.id);
if (inUse) {
throw ValidationException('Cannot delete equipment in use');
}
}
}Entity Transformations
abstract class EntityTransformer<S extends EntityBase, T extends EntityBase> {
T transform(S source);
S? reverseTransform(T target);
}
class UserToPersonTransformer extends EntityTransformer<User, Person> {
@override
Person transform(User user) {
return Person(
id: user.id,
schemaType: 'persons',
firstName: user.name.split(' ').first,
lastName: user.name.split(' ').skip(1).join(' '),
email: user.email,
phoneNumber: user.phone,
createdAt: user.createdAt,
);
}
@override
User? reverseTransform(Person person) {
// Reverse transformation might not always be possible
return null;
}
}Performance Optimization
Entity Caching
class EntityCache<T extends EntityBase> {
final Map<String, T> _cache = {};
final Map<String, DateTime> _timestamps = {};
final Duration ttl;
EntityCache({this.ttl = const Duration(minutes: 5)});
T? get(String id) {
final timestamp = _timestamps[id];
if (timestamp != null &&
DateTime.now().difference(timestamp) > ttl) {
_cache.remove(id);
_timestamps.remove(id);
return null;
}
return _cache[id];
}
void put(T entity) {
_cache[entity.id] = entity;
_timestamps[entity.id] = DateTime.now();
}
void invalidate(String id) {
_cache.remove(id);
_timestamps.remove(id);
}
void clear() {
_cache.clear();
_timestamps.clear();
}
}Query Optimization
class OptimizedEntityQuery<T extends EntityBase> {
final EntityApi<T> api;
OptimizedEntityQuery({required this.api});
Stream<List<T>> watchList({
required QueryOptions options,
Duration pollInterval = const Duration(seconds: 30),
}) async* {
var lastResult = <T>[];
var lastHash = '';
while (true) {
try {
final result = await api.list(
offset: options.offset,
limit: options.limit,
sortBy: options.sortBy,
);
final currentHash = _computeHash(result);
if (currentHash != lastHash) {
lastResult = result;
lastHash = currentHash;
yield result;
}
} catch (e) {
// Yield last known good result on error
yield lastResult;
}
await Future.delayed(pollInterval);
}
}
String _computeHash(List<T> entities) {
final ids = entities.map((e) => e.id).join(',');
return ids.hashCode.toString();
}
}Best Practices
- Immutability - Prefer immutable updates with copyWith
- Validation - Validate at every lifecycle stage
- Relationships - Define clear relationship patterns
- Performance - Use caching and lazy loading appropriately
- Hooks - Implement lifecycle hooks for complex logic
- Versioning - Track changes for audit and rollback
- Error Handling - Handle lifecycle errors gracefully
Next: Layouts and Views - Complete guide to the layout system