/* EODatabaseChannel.m Copyright (C) 1996 Free Software Foundation, Inc. Author: Mircea Oancea Date: 1996 This file is part of the GNUstep Database Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #import "common.h" #import "EODatabaseChannel.h" #import "EOAdaptor.h" #import "EOAdaptorChannel.h" #import "EOAdaptorContext.h" #import "EOAttribute.h" #import "EODatabase.h" #import "EODatabaseContext.h" #import "EOEntity.h" #import "EODatabaseFault.h" #import "EOGenericRecord.h" #import "EOModel.h" #import "EOObjectUniquer.h" #import "EOSQLQualifier.h" #import "EORelationship.h" #import #import #import @class EOGenericRecord; NSString *EODatabaseChannelWillOpenNotificationName = @"EODatabaseChannelWillOpenNotification"; NSString *EODatabaseChannelDidOpenNotificationName = @"EODatabaseChannelDidOpenNotification"; NSString *EODatabaseChannelWillCloseNotificationName = @"EODatabaseChannelWillCloseNotification"; NSString *EODatabaseChannelDidCloseNotificationName = @"EODatabaseChannelDidCloseNotification"; NSString *EODatabaseChannelCouldNotOpenNotificationName = @"EODatabaseChannelCouldNotOpenNotification"; NSString *EODatabaseChannelWillInsertObjectName = @"EODatabaseChannelWillInsertObjectName"; NSString *EODatabaseChannelDidInsertObjectName = @"EODatabaseChannelDidInsertObjectName"; NSString *EODatabaseChannelWillUpdateObjectName = @"EODatabaseChannelWillUpdateObjectName"; NSString *EODatabaseChannelDidUpdateObjectName = @"EODatabaseChannelDidUpdateObjectName"; NSString *EODatabaseChannelWillDeleteObjectName = @"EODatabaseChannelWillDeleteObjectName"; NSString *EODatabaseChannelDidDeleteObjectName = @"EODatabaseChannelDidDeleteObjectName"; NSString *EODatabaseChannelWillLockObjectName = @"EODatabaseChannelWillLockObjectName"; NSString *EODatabaseChannelDidLockObjectName = @"EODatabaseChannelDidLockObjectName"; /* * Private methods declaration */ @interface EODatabaseChannel(Private) - (id)privateFetchWithZone:(NSZone *)_zone; - (Class)privateClassForEntity:(EOEntity *)anEntity; - (void)privateUpdateCurrentEntityInfo; - (void)privateClearCurrentEntityInfo; - (void)privateReportError:(SEL)method:(NSString *)format, ...; @end /* * EODatabaseChannel implementation */ @implementation EODatabaseChannel /* * Initializing a new instance */ - (id)initWithDatabaseContext:(EODatabaseContext *)_dbContext { if (_dbContext == nil) { AUTORELEASE(self); return nil; } self->notificationCenter = RETAIN([NSNotificationCenter defaultCenter]); self->databaseContext = RETAIN(_dbContext); [self setDelegate:[self->databaseContext delegate]]; [self->databaseContext channelDidInit:self]; return self; } - (void)dealloc { [self->databaseContext channelWillDealloc:self]; RELEASE(self->currentEditingContext); RELEASE(self->databaseContext); RELEASE(self->adaptorChannel); RELEASE(self->notificationCenter); [super dealloc]; } // notifications - (void)postNotification:(NSString *)_name { [self->notificationCenter postNotificationName:_name object:self]; } - (void)postNotification:(NSString *)_name object:(id)_obj { [self->notificationCenter postNotificationName:_name object:self userInfo:[NSDictionary dictionaryWithObject:_obj forKey:@"object"]]; } // accessors - (EOAdaptorChannel *)adaptorChannel { if (self->adaptorChannel == nil) { static int reuseAdaptorCh = -1; if (reuseAdaptorCh == -1) { reuseAdaptorCh = [[[NSUserDefaults standardUserDefaults] objectForKey:@"EOReuseAdaptorChannel"] boolValue] ? 1 : 0; } if (reuseAdaptorCh) { NSEnumerator *channels; EOAdaptorChannel *channel; channels = [[[[self databaseContext] adaptorContext] channels] objectEnumerator]; while ((channel = [channels nextObject])) { if ([channel isFetchInProgress]) continue; #if DEBUG NSLog(@"reuse adaptor channel: %@", channel); #endif self->adaptorChannel = channel; break; } } if (self->adaptorChannel == nil) { self->adaptorChannel = [[[self databaseContext] adaptorContext] createAdaptorChannel]; } RETAIN(self->adaptorChannel); } return self->adaptorChannel; } - (EODatabaseContext *)databaseContext { return self->databaseContext; } // delegate - (void)setDelegate:(id)_delegate { self->delegate = _delegate; } - (id)delegate { return self->delegate; } // Opening and closing a channel - (BOOL)isOpen { return [[self adaptorChannel] isOpen]; } - (BOOL)openChannel { BOOL result; [self postNotification:EODatabaseChannelWillOpenNotificationName]; if ((result = [[self adaptorChannel] openChannel])) { self->successfulOpenCount++; [self postNotification:EODatabaseChannelDidOpenNotificationName]; } else { self->failedOpenCount++; [self postNotification:EODatabaseChannelCouldNotOpenNotificationName]; } return result; } - (void)closeChannel { [self postNotification:EODatabaseChannelWillCloseNotificationName]; [[self adaptorChannel] closeChannel]; self->closeCount++; [self postNotification:EODatabaseChannelDidCloseNotificationName]; } // Modifying objects - (BOOL)_isNoRaiseOnModificationException:(NSException *)_exception { /* for compatibility with non-X methods, translate some errors to a bool */ NSString *n; n = [_exception name]; if ([n isEqualToString:@"EOEvaluationError"]) return YES; if ([n isEqualToString:@"EODelegateRejects"]) return YES; return NO; } - (BOOL)insertObject:(id)anObj { // TODO: split up this method // TODO: write an insertObjectX: which returns an exception NSException *exception = nil; EOEntity *entity = nil; NSDictionary *pkey = nil; NSDictionary *values = nil; NSDictionary *snapshot = nil; NSArray *attributes = nil; int i; [self postNotification:EODatabaseChannelWillInsertObjectName object:anObj]; if (![anObj prepareForInsertInChannel:self context:self->databaseContext]) return NO; // Check the delegate if ([self->delegate respondsToSelector: @selector(databaseChannel:willInsertObject:)]) anObj = [delegate databaseChannel:self willInsertObject:anObj]; // Check nil (delegate disallowes or given object was nil) if (anObj == nil) return NO; // Check if fault if ([EOFault isFault:anObj]) { [NSException raise:NSInvalidArgumentException format:@"Attempt to insert a fault in a database channel"]; } // Check if we can insert if ([databaseContext updateStrategy] == EONoUpdate) { [self privateReportError:_cmd : @"cannot insert if context has 'NoUpdate' update strategy."]; return NO; } /* validate object for insert */ if ((exception = [anObj validateForInsert])) { /* validation failed */ [exception raise]; } // Check if in a transaction if (![databaseContext transactionNestingLevel]) { [self privateReportError:_cmd : @"cannot insert if contex has no transaction opened."]; return NO; } // Get entity entity = [anObj respondsToSelector:@selector(entity)] ? [anObj entity] : [[[[adaptorChannel adaptorContext] adaptor] model] entityForObject:anObj]; // Check entity if (entity == nil) { [self privateReportError:_cmd : @"cannot determine entity for object %p class %@.", anObj, NSStringFromClass([anObj class])]; return NO; } if ([entity isReadOnly]) { [self privateReportError:_cmd : @"cannot insert object %p for readonly entity %@.", anObj, [entity name]]; return NO; } // Get array of attributes to insert attributes = [entity attributesUsedForInsert]; // Get simple values and convert them to adaptor values values = [anObj valuesForKeys:[entity attributesNamesUsedForInsert]]; values = [entity convertValuesToModel:values]; // Get and check *must insert* attributes (primary key, lock, readonly) for (i = [attributes count]-1; i >= 0; i--) { EOAttribute *attribute = [attributes objectAtIndex:i]; NSAssert(attribute, @"invalid attribute object .."); if (![values objectForKey:[attribute name]]) { [self privateReportError:_cmd : @"null value for insert attribute %@ for object %@ entity %@", [attribute name], anObj, [entity name]]; return NO; } } // Make primary key and snapshot snapshot = [entity snapshotForRow:values]; if (snapshot == nil) { [self privateReportError:_cmd : @"cannot determine snapshot for %p from values %@ entity %@.", anObj, [values description], [entity name]]; return NO; } pkey = [entity primaryKeyForRow:values]; if (pkey == nil) { [self privateReportError:_cmd : @"cannot determine primary key for %p from values %@ entity %@.", anObj, [values description], [entity name]]; return NO; } // Insert adaptor row exception = [adaptorChannel insertRowX:values forEntity:entity]; if (exception) { if (![self _isNoRaiseOnModificationException:exception]) [exception raise]; return NO; } // Record object in database context [databaseContext recordObject:anObj primaryKey:pkey entity:entity snapshot:values]; self->insertCount++; [anObj wasInsertedInChannel:self context:self->databaseContext]; // Notify delegate if ([delegate respondsToSelector:@selector(databaseChannel:didInsertObject:)]) [delegate databaseChannel:self didInsertObject:anObj]; [self postNotification:EODatabaseChannelDidInsertObjectName object:anObj]; return YES; } - (BOOL)updateObject:(id)anObj { // TODO: split up this huge method // TODO: make an updateObjectX: method which returns an exception NSException *exception = nil; EOEntity *entity = nil; EOSQLQualifier *qualifier = nil; NSDictionary *old_pkey = nil; NSDictionary *old_snapshot = nil; NSDictionary *new_pkey = nil; NSDictionary *new_snapshot = nil; NSDictionary *values = nil; BOOL needsOptimisticLock; [self postNotification:EODatabaseChannelWillUpdateObjectName object:anObj]; if (![anObj prepareForUpdateInChannel:self context:self->databaseContext]) return NO; // Check the delegate if ([delegate respondsToSelector:@selector(databaseChannel:willUpdateObject:)]) anObj = [delegate databaseChannel:self willUpdateObject:anObj]; // Check nil (delegate disallowes or given object was nil) if (anObj == nil) return NO; // Check if fault if ([EOFault isFault:anObj]) { [NSException raise:NSInvalidArgumentException format:@"Attempt to update a fault in a database channel"]; } // Check if we can update if ([databaseContext updateStrategy] == EONoUpdate) { [self privateReportError:_cmd : @"cannot update if context has 'NoUpdate' update strategy."]; return NO; } /* validate object for update */ if ((exception = [anObj validateForUpdate])) { /* validation failed */ [exception raise]; } // Check if in a transaction if (![databaseContext transactionNestingLevel]) { [self privateReportError:_cmd : @"cannot update if contex has no transaction opened."]; return NO; } // Get entity entity = [anObj respondsToSelector:@selector(entity)] ? [anObj entity] : [[[[adaptorChannel adaptorContext] adaptor] model] entityForObject:anObj]; // Check entity { if (entity == nil) { [self privateReportError:_cmd : @"cannot determine entity for object %p class %@.", anObj, NSStringFromClass([anObj class])]; return NO; } if ([entity isReadOnly]) { [self privateReportError:_cmd : @"cannot update object %p for readonly entity %@.", anObj, [entity name]]; return NO; } } // Get and check old snapshot and primary key { [databaseContext primaryKey:&old_pkey andSnapshot:&old_snapshot forObject:anObj]; if (old_snapshot == nil) { [self privateReportError:_cmd : @"cannot update object %p because there is no snapshot for it."]; return NO; } if (old_pkey == nil) old_pkey = [entity primaryKeyForRow:old_snapshot]; if (old_pkey == nil) { [self privateReportError:_cmd : @"cannot determine primary key for %p from snapshot %@ entity %@.", anObj, [old_snapshot description], [entity name]]; return NO; } } // Get simple values and convert them to adaptor values values = [anObj valuesForKeys:[entity attributesNamesUsedForInsert]]; values = [entity convertValuesToModel:values]; // Get and check new primary key and snapshot { new_snapshot = [entity snapshotForRow:values]; if (new_snapshot == nil) { [self privateReportError:_cmd : @"cannot determine snapshot for %p from values %@ entity %@.", anObj, [values description], [entity name]]; return NO; } new_pkey = [entity primaryKeyForRow:new_snapshot]; if (new_pkey == nil) { [self privateReportError:_cmd : @"cannot determine primary key for %p from values %@ entity %@.", anObj, [values description], [entity name]]; return NO; } } // Check if we need to lock optimistic before update // that is compare locking attributes with the existing ones in database switch([databaseContext updateStrategy]) { case EOUpdateWithOptimisticLocking: case EOUpdateWithPessimisticLocking: needsOptimisticLock = ![databaseContext isObjectLocked:anObj]; break; case EOUpdateWithNoLocking: needsOptimisticLock = NO; break; default: return NO; } // If we need an "optimistic lock" then perform lock // else just make the qualifier based on the primary key only if (needsOptimisticLock) { int i; BOOL canUseQualifier = YES; NSArray *lockAttrs = [entity attributesUsedForLocking]; EOAdaptor *adaptor = [[adaptorChannel adaptorContext] adaptor]; NSDictionary *row; // Check if attributes used for locking can be used in a qualifier for (i = [lockAttrs count]-1; i >= 0; i--) { if (![adaptor isValidQualifierType: [[lockAttrs objectAtIndex:i] externalType]]) { canUseQualifier = NO; break; } } if (canUseQualifier) // If YES just build the qualifier qualifier = [EOSQLQualifier qualifierForRow:old_snapshot entity:entity]; else { // If NO then lock the row in the database server, fetch the // new snapshot and compare it with the old one qualifier = [EOSQLQualifier qualifierForPrimaryKey:old_pkey entity:entity]; #ifdef DEBUG NSAssert2([lockAttrs count] > 0, @"missing locking attributes: lock=%@ object=%@", lockAttrs, anObj); #endif if (![adaptorChannel selectAttributes:lockAttrs describedByQualifier:qualifier fetchOrder:nil lock:YES]) { [self privateReportError:_cmd : @"could not lock=%@ with qualifier=%@ entity=%@.", anObj, [qualifier description], [entity name]]; return NO; } row = [adaptorChannel fetchAttributes:lockAttrs withZone:NULL]; [adaptorChannel cancelFetch]; if (row == nil) { [self privateReportError:_cmd : @"could not get row to lock %p with qualifier %@.", anObj, [qualifier description]]; return NO; } [databaseContext recordLockedObject:anObj]; if (![row isEqual:old_snapshot]) { [self privateReportError:_cmd : @"could not lock %p. Snapshots: self %@ database %@.", anObj, [old_snapshot description], [row description]]; return NO; } } } else { qualifier = [EOSQLQualifier qualifierForPrimaryKey:old_pkey entity:entity]; } // Compute values as delta from values and old_snapshot { NSMutableDictionary *delta; NSString *attributeName; NSArray *allKeys; int i, count; allKeys = [values allKeys]; delta = [NSMutableDictionary dictionary]; for (i = 0, count = [allKeys count]; i < count; i++) { id new_v, old_v; attributeName = [allKeys objectAtIndex:i]; new_v = [values objectForKey:attributeName]; old_v = [old_snapshot objectForKey:attributeName]; if ((old_v == nil) || ![new_v isEqual:old_v]) [delta setObject:new_v forKey:attributeName]; } values = delta; } // no reason for update --> fetch to be sure, that no one has deleted it // HH: The object was not changed, so we refetch to determine whether it // was deleted if ([values count] == 0) { if (![self refetchObject:anObj]) return NO; } // Update in adaptor else { NSException *ex; ex = [adaptorChannel updateRowX:values describedByQualifier:qualifier]; if (ex != nil) { if (![self _isNoRaiseOnModificationException:ex]) [ex raise]; return NO; } } // Record object in database context if (![new_pkey isEqual:old_pkey]) { NSLog(@"WARNING: (%@) primary key changed from %@ to %@", __PRETTY_FUNCTION__, old_pkey, new_pkey); [databaseContext forgetObject:anObj]; } [databaseContext recordObject:anObj primaryKey:new_pkey entity:entity snapshot:new_snapshot]; [databaseContext recordUpdatedObject:anObj]; self->updateCount++; [anObj wasUpdatedInChannel:self context:self->databaseContext]; // Notify delegate if ([delegate respondsToSelector:@selector(databaseChannel:didUpdateObject:)]) [delegate databaseChannel:self didUpdateObject:anObj]; [self postNotification:EODatabaseChannelDidUpdateObjectName object:anObj]; return YES; } - (BOOL)deleteObject:(id)anObj { // TODO: split this method // TODO: add an deleteObjectX: method which returns an NSException NSException *exception = nil; EOEntity *entity = nil; NSDictionary *pkey = nil; NSDictionary *snapshot = nil; EOSQLQualifier *qualifier = nil; [self postNotification:EODatabaseChannelWillDeleteObjectName object:anObj]; if (![anObj prepareForDeleteInChannel:self context:self->databaseContext]) return NO; // Check the delegate if ([delegate respondsToSelector:@selector(databaseChannel:willDeleteObject:)]) anObj = [delegate databaseChannel:self willDeleteObject:anObj]; // Check nil (delegate disallowes or given object was nil) if (anObj == nil) return NO; // Check if fault if ([EOFault isFault:anObj]) { [NSException raise:NSInvalidArgumentException format:@"Attempt to delete a fault in a database channel"]; } // Check if we can delete if ([databaseContext updateStrategy] == EONoUpdate) { [self privateReportError:_cmd : @"cannot delete if context has 'NoUpdate' update strategy."]; return NO; } /* validate object for delete */ if ((exception = [anObj validateForDelete])) { /* validation failed */ [exception raise]; } // Check if in a transaction if (![databaseContext transactionNestingLevel]) { [self privateReportError:_cmd : @"cannot update if contex has no transaction opened."]; return NO; } // Get entity entity = [anObj respondsToSelector:@selector(entity)] ? [anObj entity] : [[[[adaptorChannel adaptorContext] adaptor] model] entityForObject:anObj]; // Check entity if (entity == nil) { [self privateReportError:_cmd : @"cannot determine entity for object %p class %s.", anObj, NSStringFromClass([anObj class])]; return NO; } if ([entity isReadOnly]) { [self privateReportError:_cmd : @"cannot delete object %p for readonly entity %@.", anObj, [entity name]]; return NO; } // Get snapshot and old primary key [databaseContext primaryKey:&pkey andSnapshot:&snapshot forObject:anObj]; if (pkey == nil) { if (snapshot == nil) [self privateReportError:_cmd : @"cannot delete object %p because there is no snapshot for it."]; pkey = [entity primaryKeyForRow:snapshot]; } if (pkey == nil) { [self privateReportError:_cmd : @"cannot determine primary key for %p from values %@ entity %@.", anObj, [snapshot description], [entity name]]; return NO; } // Get and check qualifier for object to delete qualifier = [EOSQLQualifier qualifierForPrimaryKey:pkey entity:entity]; if (qualifier == nil) { [self privateReportError:_cmd : @"cannot make qualifier to delete %p primary key %@ entity %@.", anObj, [pkey description], [entity name]]; return NO; } // Delete adaptor row exception = [adaptorChannel deleteRowsDescribedByQualifierX:qualifier]; if (exception != nil) { if (![self _isNoRaiseOnModificationException:exception]) [exception raise]; return NO; } AUTORELEASE(RETAIN(anObj)); // Forget object in database context [databaseContext forgetObject:anObj]; self->deleteCount++; [anObj wasDeletedInChannel:self context:self->databaseContext]; // Notify delegate if ([delegate respondsToSelector: @selector(databaseChannel:didDeleteObject:)]) [delegate databaseChannel:self didDeleteObject:anObj]; [self postNotification:EODatabaseChannelDidDeleteObjectName object:anObj]; return YES; } - (BOOL)lockObject:(id)anObj { EOEntity *entity = nil; NSDictionary *pkey = nil; NSDictionary *snapshot = nil; EOSQLQualifier *qualifier = nil; [self postNotification:EODatabaseChannelWillLockObjectName object:anObj]; if (![anObj prepareForLockInChannel:self context:self->databaseContext]) return NO; // Check the delegate if ([delegate respondsToSelector:@selector(databaseChannel:willLockObject:)]) anObj = [delegate databaseChannel:self willLockObject:anObj]; // Check nil (delegate disallowes or given object was nil) if (anObj == nil) return NO; // Check if fault if ([EOFault isFault:anObj]) { [NSException raise:NSInvalidArgumentException format:@"Attempt to lock a fault in a database channel"]; } // Check if we can lock if ([databaseContext updateStrategy] == EONoUpdate) { [self privateReportError:_cmd : @"cannot lock if context has 'NoUpdate' update strategy."]; return NO; } // Check if in a transaction if (![databaseContext transactionNestingLevel]) { [self privateReportError:_cmd : @"cannot lock if contex has no transaction opened."]; return NO; } // Check if fetch is in progress if ([self isFetchInProgress]) { [self privateReportError:_cmd : @"cannot lock if contex has a fetch in progress."]; return NO; } // Get entity entity = [anObj respondsToSelector:@selector(entity)] ? [anObj entity] : [[[[adaptorChannel adaptorContext] adaptor] model] entityForObject:anObj]; // Check entity if (entity == nil) { [self privateReportError:_cmd : @"cannot determine entity for object %p class %s.", anObj, NSStringFromClass([anObj class])]; return NO; } if ([entity isReadOnly]) { [self privateReportError:_cmd : @"cannot lock object %p for readonly entity %@.", anObj, [entity name]]; return NO; } // Get snapshot and old primary key [databaseContext primaryKey:&pkey andSnapshot:&snapshot forObject:anObj]; if (snapshot == nil) { [self privateReportError:_cmd : @"cannot lock object %p because there is no snapshot for it."]; return NO; } if (pkey == nil) pkey = [entity primaryKeyForRow:snapshot]; if (pkey == nil) { [self privateReportError:_cmd : @"cannot determine primary key for %p from values %@ entity %@.", anObj, [snapshot description], [entity name]]; return NO; } { NSArray *lockAttrs = [entity attributesUsedForLocking]; NSDictionary *row = nil; qualifier = [EOSQLQualifier qualifierForPrimaryKey:pkey entity:entity]; #ifdef DEBUG NSAssert2([lockAttrs count] > 0, @"missing locking attributes: lock=%@ object=%@", lockAttrs, anObj); #endif if (![adaptorChannel selectAttributes:lockAttrs describedByQualifier:qualifier fetchOrder:nil lock:YES]) { [self privateReportError:_cmd : @"could not lock %p with qualifier %@.", anObj, [qualifier description]]; return NO; } row = [adaptorChannel fetchAttributes:lockAttrs withZone:NULL]; [adaptorChannel cancelFetch]; if (row == nil) { [self privateReportError:_cmd : @"could not lock %p with qualifier %@.", anObj, [qualifier description]]; return NO; } if (![row isEqual:snapshot]) { [self privateReportError:_cmd : @"could not lock %p. Snapshots: self %@ database %@.", anObj, [snapshot description], [row description]]; return NO; } } // Register lock object in database context [databaseContext recordLockedObject:anObj]; self->lockCount++; [anObj wasLockedInChannel:self context:self->databaseContext]; // Notify delegate if ([delegate respondsToSelector:@selector(databaseChannel:didLockObject:)]) [delegate databaseChannel:self didLockObject:anObj]; [self postNotification:EODatabaseChannelDidLockObjectName object:anObj]; return YES; } - (BOOL)refetchObject:(id)anObj { EOEntity *entity = nil; NSDictionary *pkey = nil; NSDictionary *snapshot = nil; EOSQLQualifier *qualifier = nil; // Check the delegate if ([delegate respondsToSelector: @selector(databaseChannel:willRefetchObject:)]) anObj = [delegate databaseChannel:self willRefetchObject:anObj]; // Check nil (delegate disallowes or given object was nil) if (anObj == nil) return NO; // Check if fault if ([EOFault isFault:anObj]) { [NSException raise:NSInvalidArgumentException format:@"Attempt to refetch a fault in a database channel"]; } // Check if in a transaction if (![databaseContext transactionNestingLevel]) { [self privateReportError:_cmd : @"cannot refetch if context has no transaction opened."]; return NO; } // Check if fetch is in progress if ([self isFetchInProgress]) { [self privateReportError:_cmd : @"cannot refetch if context has a fetch in progress."]; return NO; } // Get entity entity = [anObj respondsToSelector:@selector(entity)] ? [anObj entity] : [[[[adaptorChannel adaptorContext] adaptor] model] entityForObject:anObj]; // Check entity if (entity == nil) { [self privateReportError:_cmd : @"cannot determine entity for object %p class %s.", anObj, NSStringFromClass([anObj class])]; return NO; } // Get snapshot and old primary key [databaseContext primaryKey:&pkey andSnapshot:&snapshot forObject:anObj]; if (pkey == nil) { if (snapshot == nil) [self privateReportError:_cmd : @"cannot refetch object %p because there is no snapshot for it."]; pkey = [entity primaryKeyForRow:snapshot]; } if (pkey == nil) { [self privateReportError:_cmd : @"cannot determine primary key for %p from values %@ entity %@.", anObj, [snapshot description], [entity name]]; return NO; } // Get and check qualifier for object to refetch qualifier = [EOSQLQualifier qualifierForPrimaryKey:pkey entity:entity]; if (qualifier == nil) { [self privateReportError:_cmd : @"cannot make qualifier to refetch %p primary key %@ entity %@.", anObj, [pkey description], [entity name]]; return NO; } // Request object from adaptor [self setCurrentEntity:entity]; [self privateUpdateCurrentEntityInfo]; if (currentAttributes == nil) { [self privateReportError:_cmd : @"internal inconsitency while refetching %p.", anObj]; return NO; } #ifdef DEBUG NSAssert3([currentAttributes count] > 0, @"missing attributes for select: lock=%@ object=%@ entity=%@", currentAttributes, anObj, entity); #endif if (![adaptorChannel selectAttributes:currentAttributes describedByQualifier:qualifier fetchOrder:nil lock:([databaseContext updateStrategy] == EOUpdateWithPessimisticLocking)]) { [self privateClearCurrentEntityInfo]; return NO; } // Get object from adaptor, re-build its faults and record new snapshot anObj = [self privateFetchWithZone:NULL]; [self cancelFetch]; if (anObj == nil) { [self privateReportError:_cmd : @"could not refetch %p with qualifier %@.", anObj, [qualifier description]]; return NO; } // Notify delegate if ([delegate respondsToSelector:@selector(databaseChannel:didRefetchObject:)]) [delegate databaseChannel:self didRefetchObject:anObj]; return YES; } - (id)_createObjectForRow:(NSDictionary*)aRow entity:(EOEntity*)anEntity isPrimaryKey:(BOOL)yn zone:(NSZone*)zone { Class class = Nil; id anObj = nil; if (anEntity == nil) return nil; class = [self privateClassForEntity:anEntity]; // Create new instance if ([class respondsToSelector:@selector(classForEntity:values:)]) class = [class classForEntity:anEntity values:aRow]; anObj = [class allocWithZone:zone]; return anObj; } - (id)allocateObjectForRow:(NSDictionary *)row entity:(EOEntity *)anEntity zone:(NSZone *)zone { Class class = Nil; id anObj = nil; if (anEntity == nil) return nil; class = [self privateClassForEntity:anEntity]; // Create new instance if ([class respondsToSelector:@selector(classForEntity:values:)]) class = [class classForEntity:anEntity values:row]; anObj = [class allocWithZone:zone]; return anObj; } - (id)initializedObjectForRow:(NSDictionary *)row entity:(EOEntity *)anEntity zone:(NSZone *)zone { id anObj; anObj = [self allocateObjectForRow:row entity:anEntity zone:zone]; anObj = [anObj respondsToSelector:@selector(initWithPrimaryKey:entity:)] ? [anObj initWithPrimaryKey:row entity:anEntity] : [anObj init]; return AUTORELEASE(anObj); } /* * Fetching objects */ - (id)_fetchObject:(id)anObj qualifier:(EOSQLQualifier *)qualifier { id obj; [self selectObjectsDescribedByQualifier:qualifier fetchOrder:nil]; obj = [self fetchWithZone:NULL]; [self cancelFetch]; return obj; } - (BOOL)selectObjectsDescribedByQualifier:(EOSQLQualifier *)qualifier fetchOrder:(NSArray *)fetchOrder { if ([delegate respondsToSelector: @selector(databaseChannel:willSelectObjectsDescribedByQualifier:fetchOrder:)]) if (![delegate databaseChannel:self willSelectObjectsDescribedByQualifier:qualifier fetchOrder:fetchOrder]) return NO; [self setCurrentEntity:[qualifier entity]]; [self privateUpdateCurrentEntityInfo]; if (self->currentAttributes == nil) { [self privateReportError:_cmd : @"internal inconsitency while selecting."]; } #ifdef DEBUG NSAssert3([self->currentAttributes count] > 0, @"missing select attributes: attrs=%@, qualifier=%@, entity=%@", self->currentAttributes, qualifier, self->currentEntity); #endif if (![adaptorChannel selectAttributes:self->currentAttributes describedByQualifier:qualifier fetchOrder:fetchOrder lock:([databaseContext updateStrategy] == EOUpdateWithPessimisticLocking)]) { [self privateClearCurrentEntityInfo]; [self privateReportError:_cmd : @"could not select attributes with qualifier %@.", [qualifier description]]; return NO; } if ([delegate respondsToSelector: @selector(databaseChannel:didSelectObjectsDescribedByQualifier:fetchOrder:)]) [delegate databaseChannel:self didSelectObjectsDescribedByQualifier:qualifier fetchOrder:fetchOrder]; return YES; } - (id)fetchWithZone:(NSZone *)zone { id object = nil; if ([delegate respondsToSelector: @selector(databaseChannel:willFetchObjectOfClass:withZone:)]) { Class class; class = currentClass ? currentClass : [self privateClassForEntity:currentEntity]; [delegate databaseChannel:self willFetchObjectOfClass:class withZone:zone]; } object = [self privateFetchWithZone:zone]; if (object == nil) return nil; if ([delegate respondsToSelector:@selector(databaseChannel:didFetchObject:)]) [delegate databaseChannel:self didFetchObject:object]; return object; } - (BOOL)isFetchInProgress { return [[self adaptorChannel] isFetchInProgress]; } - (void)cancelFetch { if ([[self adaptorChannel] isFetchInProgress]) { [self privateClearCurrentEntityInfo]; [[self adaptorChannel] cancelFetch]; } } - (void)setCurrentEntity:(EOEntity *)_entity { // Clear entity info [self privateClearCurrentEntityInfo]; // Set new entity NSAssert(self->currentEntity == nil, @"entity not cleared correctly .."); self->currentEntity = RETAIN(_entity); } - (void)privateClearCurrentEntityInfo { RELEASE(self->currentEntity); self->currentEntity = nil; RELEASE(self->currentAttributes); self->currentAttributes = nil; RELEASE(self->currentRelations); self->currentRelations = nil; self->currentClass = Nil; self->currentReady = NO; } - (void)privateUpdateCurrentEntityInfo { if (self->currentEntity == nil) { [NSException raise:NSInvalidArgumentException format:@"Must use setCurrentEntity if select is not done " @"through database"]; } if (self->currentAttributes == nil) self->currentAttributes = RETAIN([self->currentEntity attributesUsedForFetch]); if (self->currentRelations == nil) self->currentRelations = RETAIN([self->currentEntity relationsUsedForFetch]); self->currentReady = YES; } /* * Private methods */ - (Class)privateClassForEntity:(EOEntity *)anEntity { Class class; if (anEntity == currentEntity && currentClass) return currentClass; // Get class for new object class = NSClassFromString([anEntity className]); if (!class && [delegate respondsToSelector: @selector(databaseChannel:failedToLookupClassNamed:)]) class = [delegate databaseChannel:self failedToLookupClassNamed:[[anEntity className] cString]]; if (class == Nil) class = [EOGenericRecord class]; if (anEntity == currentEntity) currentClass = class; return class; } - (id)privateFetchWithZone:(NSZone *)_zone { NSMutableDictionary *values = nil; id object = nil; NSDictionary *pkey = nil; NSDictionary *snapshot = nil; NSDictionary *row = nil; NSDictionary *dict = nil;; // Be sure we have entity info (raises if no entity is set) if (!self->currentReady) [self privateUpdateCurrentEntityInfo]; // fetch row from adaptor row = [[self adaptorChannel] fetchAttributes:self->currentAttributes withZone:_zone]; if (row == nil) // Results set finished or no more result sets return nil; #if 0 row = [row copyWithZone:_zone]; AUTORELEASE(row); #endif // determine primary key and snapshot snapshot = [self->currentEntity snapshotForRow:row]; pkey = [self->currentEntity primaryKeyForRow:row]; if ((pkey == nil) || (snapshot == nil)) { // TODO - should we have a delegate method here ? [NSException raise:NSInvalidArgumentException format:@"Cannot determine primary key and snapshot for row"]; } // lookup object in context/database object = [self->databaseContext objectForPrimaryKey:pkey entity:currentEntity]; // use old, make new, clear fault if (object == nil) { //NSLog(@"new anObj\n"); object = [self initializedObjectForRow:row entity:currentEntity zone:_zone]; } if ([EOFault isFault:object]) { [EODatabaseFault clearFault:object]; object = [object respondsToSelector:@selector(initWithPrimaryKey:entity:)] ? [object initWithPrimaryKey:row entity:currentEntity] : [object init]; if (object == nil) { [NSException raise:NSInvalidArgumentException format:@"could not initialize cleared fault with " @"row `%@' and entity %@", [row description], [currentEntity name]]; } } // make values // TODO - handle only class properties to object values = [NSMutableDictionary dictionaryWithCapacity: ([row count] + [currentRelations count])]; [values addEntriesFromDictionary:row]; // resolve relationships (to-one and to-many) { EORelationship *rel = nil; int i, n = [self->currentRelations count]; id fault = nil; for (i = 0; i < n; i++) { rel = [self->currentRelations objectAtIndex:i]; // Check if the delegate can provide a different relationship if ([delegate respondsToSelector: @selector(databaseChannel:relationshipForRow:relationship:)]) { id nrel = [delegate databaseChannel:self relationshipForRow:row relationship:rel]; rel = nrel ? nrel : (id)rel; } if ([rel isToMany]) { // Build to-many fault EOSQLQualifier* qualifier = [EOSQLQualifier qualifierForRow:row relationship:rel]; if (qualifier == nil) { // HH: THROW was uncommented .. [NSException raise:NSInvalidArgumentException format: @"Cannot build fault qualifier for relationship"]; // TODO continue; } #if LIB_FOUNDATION_LIBRARY if ([NSClassFromString([[rel destinationEntity] className]) isGarbageCollectable]) fault = [EODatabaseFault gcArrayFaultWithQualifier:qualifier fetchOrder:nil databaseChannel:self zone:_zone]; else #endif fault = [EODatabaseFault arrayFaultWithQualifier:qualifier fetchOrder:nil databaseChannel:self zone:_zone]; } else { // Build to-one fault EOEntity *faultEntity; NSDictionary *faultKey; faultEntity = [rel destinationEntity]; faultKey = [rel foreignKeyForRow:row]; faultKey = [faultEntity primaryKeyForRow:faultKey]; if (faultEntity == nil) { [NSException raise:NSInvalidArgumentException format:@"Cannot get entity for relationship"]; } if (faultKey) { fault = [self->databaseContext objectForPrimaryKey:faultKey entity:faultEntity]; if (fault == nil) { fault = [EODatabaseFault objectFaultWithPrimaryKey:faultKey entity:faultEntity databaseChannel:self zone:_zone]; [databaseContext recordObject:fault primaryKey:faultKey entity:faultEntity snapshot:nil]; } } else fault = [EONull null]; } if (fault) [values setObject:fault forKey:[rel name]]; } } // check if is updated in another context or just updated or new (delegate) dict = values; if ([[databaseContext database] isObject:object updatedOutsideContext:databaseContext]) { if ([delegate respondsToSelector: @selector(databaseChannel:willRefetchConflictingObject:withSnapshot:)]) { dict = [delegate databaseChannel:self willRefetchConflictingObject:object withSnapshot:values]; } else { [NSException raise:NSInvalidArgumentException format:@"object updated in an uncommitted transaction " @"was fetched"]; } } else { if ([delegate respondsToSelector: @selector(databaseChannel:willRefetchObject:fromSnapshot:)]) { dict = [delegate databaseChannel:self willRefetchObject:object fromSnapshot:values]; } } // does delegate disallow setting the new values and recording the fetch ? if (dict == nil) return object; // put values [object takeValuesFromDictionary:dict]; // register lock if locked if ([databaseContext updateStrategy] == EOUpdateWithPessimisticLocking) [databaseContext recordLockedObject:object]; // register object in context [databaseContext recordObject:object primaryKey:pkey entity:currentEntity snapshot:snapshot]; // awake object from database channel if ([object respondsToSelector:@selector(awakeForDatabaseChannel:)]) [object awakeForDatabaseChannel:self]; // Done. return object; } // ******************** Reporting errors ******************** - (void)privateReportError:(SEL)method :(NSString*)format,... { NSString* message; va_list va; if (![[databaseContext database] logsErrorMessages]) return; va_start(va, format); message = AUTORELEASE([[NSString alloc] initWithFormat:format arguments:va]); va_end(va); [[databaseContext database] reportErrorFormat: @"EODatabaseChannel:error in [EODatabaseChannel %@]: %@", NSStringFromSelector(method), message]; } @end /* EODatabaseChannel */ @implementation NSObject(EODatabaseChannelEONotifications) - (BOOL)prepareForDeleteInChannel:(EODatabaseChannel *)_channel context:(EODatabaseContext *)_ctx { return YES; } - (void)wasDeletedInChannel:(EODatabaseChannel *)_channel context:(EODatabaseContext *)_ctx { } - (BOOL)prepareForInsertInChannel:(EODatabaseChannel *)_channel context:(EODatabaseContext *)_ctx { return YES; } - (void)wasInsertedInChannel:(EODatabaseChannel *)_channel context:(EODatabaseContext *)_ctx { } - (BOOL)prepareForUpdateInChannel:(EODatabaseChannel *)_channel context:(EODatabaseContext *)_ctx { return YES; } - (void)wasUpdatedInChannel:(EODatabaseChannel *)_channel context:(EODatabaseContext *)_ctx { } - (BOOL)prepareForLockInChannel:(EODatabaseChannel *)_channel context:(EODatabaseContext *)_ctx { return YES; } - (void)wasLockedInChannel:(EODatabaseChannel *)_channel context:(EODatabaseContext *)_ctx { } @end /* NSObject(EODatabaseChannelNotifications) */ @implementation EODatabaseChannel(Statistics) - (unsigned int)successfulOpenCount { return self->successfulOpenCount; } - (unsigned int)failedOpenCount { return self->failedOpenCount; } - (unsigned int)closeCount { return self->closeCount; } - (unsigned int)insertCount { return self->insertCount; } - (unsigned int)updateCount { return self->updateCount; } - (unsigned int)deleteCount { return self->deleteCount; } - (unsigned int)lockCount { return self->lockCount; } @end