# -*- coding: iso-8859-1 -*-
#-----------------------------------------------------------------------------
# Modeling Framework: an Object-Relational Bridge for python
#
# Copyright (c) 2001-2004 Sbastien Bigaret <sbigaret@users.sourceforge.net>
# All rights reserved.
#
# This file is part of the Modeling Framework.
#
# This code is distributed under a "3-clause BSD"-style license;
# see the LICENSE file for details.
#-----------------------------------------------------------------------------
from __future__ import nested_scopes
"""
DatabaseContext
Updating/Locking strategies: __TBD
- UPDATE_WITH_NO_LOCKING
- UPDATE_WITH_OPTIMISTIC_LOCKING
- UPDATE_WITH_PESSIMISTIC_LOCKING (unsupported yet)
Snapshotting
DatabaseContext uses its own local snapshotting mechanism only when it is
asked to save changes ; when changes are made, the local snapshots are
transmitted back to its Database. This means, in particular, that the
local snapshots are *not* for use while fetching (local snapshots are
anyway wiped out when the process of saving changes terminates)
Notifications
DatabaseContext as an observer
The module registers 'handleNotification()' as an observer for the
'ObjectStoreCoordinator.CooperatingObjectStoreNeeded'
DatabaseChannelNeededNotification
Posted by a DatabaseContext when it needs a DatabaseChannel.
Listeners can register a new DatabaseChannel by calling
the posting DatabaseContext's method registerChannel().
See: DatabaseContext.availableChannel()
DatabaseContext.registerChannel()
Contents:
- object: the DatabaseContext which posted the notification
- userInfo: None
CVS information
$Id: DatabaseContext.py 967 2006-02-25 13:26:05Z sbigaret $
"""
__version__='$Revision: 967 $'[11:-2]
from logging import info,error,trace,debug,warn
from NotificationFramework import NotificationCenter
NC=NotificationCenter
from mx.DateTime import DateFrom
import sys
#trace=lambda msg: sys.stderr.write(str(msg)+'\n')
# Framework
from Database import DistantPastTimeInterval,GeneralDatabaseException
from DatabaseChannel import DatabaseChannel
from EditingContext import EditingContext
from FaultHandler import AccessFaultHandler,AccessArrayFaultHandler
from GlobalID import KeyGlobalID,GlobalIDChangedNotification
from ClassDescription import ClassDescription
from FetchSpecification import FetchSpecification
from Qualifier import QualifierOperatorEqual,qualifierToMatchAllValues,KeyValueQualifier,AndQualifier
from DatabaseOperation import DatabaseOperation,\
DATABASE_NOTHING_OPERATOR, DATABASE_INSERT_OPERATOR, \
DATABASE_UPDATE_OPERATOR, DATABASE_DELETE_OPERATOR, \
ADAPTOR_LOCK_OPERATOR, ADAPTOR_INSERT_OPERATOR, \
ADAPTOR_UPDATE_OPERATOR, ADAPTOR_DELETE_OPERATOR
from AdaptorOperation import AdaptorOperation
import SnapshotsHandling
# interfaces
from Modeling.CooperatingObjectStore import CooperatingObjectStore
# Notifications
DatabaseChannelNeededNotification='DatabaseChannelNeededNotification'
from ObjectStore import InvalidatedAllObjectsInStoreNotification
from ObjectStoreCoordinator import CooperatingObjectStoreNeededNotification
## Constants
UPDATE_WITH_NO_LOCKING=0
UPDATE_WITH_OPTIMISTIC_LOCKING=1
UPDATE_WITH_PESSIMISTIC_LOCKING=2
UPDATE_STRATEGIES=(UPDATE_WITH_NO_LOCKING,
UPDATE_WITH_OPTIMISTIC_LOCKING,
UPDATE_WITH_PESSIMISTIC_LOCKING)
## The following lock is used by all module's methods
## Remember that you should *NOT* directly access to module's variables
## since they are considered private (MT-safety)
from threading import RLock
DatabaseContextModule_lock=RLock()
lock=DatabaseContextModule_lock.acquire
unlock=DatabaseContextModule_lock.release
# module's variables: see at the end of module declaration
# Modules funcs/Class 'static' methods
def contextClassToRegister():
"""
Returns the class of objects to be instanciated when an
'ObjectStoreCoordinator' object posts a 'CooperatingObjectStoreNeeded'
notification. Defaults to DatabaseContext.
"""
lock()
try:
return __contextClassToRegister
finally: unlock()
def defaultDelegate():
"""
Returns the default delegate to be assigned to DatabaseContexts at
instanciation-time. Not implemented yet (lacks an interface and specific
treatments in class 'DatabaseContext'). Defaults to None.
"""
lock()
try:
return __defaultDelegate
finally: unlock()
def forceConnectionWithModel(aModel, overrides, anEditingContext):
"""
Unimplemented yet
"""
lock()
try:
raise 'Unimplemented', 'Unimplemented yet'
finally: unlock()
def isSharedObjectLoadingEnabled():
"""
Returns '0' (false): Shared Editing Context is not supported yet.
"""
lock()
try:
return 0
finally: unlock()
def registeredDatabaseContextForModel(aModel, anObjectStore):
"""
Parameter 'anObjectStore' can be either an 'EditingContext' or a
'ObjectStoreCoordinator'. Sends message 'rootObjectStore' to
'anObjectStore' and returns the 'DatabaseContext' registered for that model.
If none is found, a new DatabaseContext object is created, registered in
the 'rootObjectStore', and returned.
"""
lock()
try:
rootObjStore=anObjectStore.rootObjectStore()
dbContexts=rootObjStore.cooperatingObjectStores()
#import pdb ; pdb.set_trace()
for dbContext in dbContexts:
if dbContext.database().addModelIfCompatible(aModel):
return dbContext #dbContext rendez-vous
# not found: create it
from Database import databaseForModel
database=databaseForModel(aModel)
dbContext=contextClassToRegister()(database)
rootObjStore.addCooperatingObjectStore(dbContext)
dbContext._coordinator=rootObjStore
return dbContext
finally: unlock()
def setContextClassToRegister(contextClass):
"""
"""
lock()
try:
global __contextClassToRegister
__contextClassToRegister=contextClass
finally: unlock()
def setDefaultDelegate(defaultDelegate):
"""
See defaultDelegate() for details
"""
lock()
try:
raise 'Unimplemented', 'Unimplemented yet'
#global __defaultDelegate
#__defaultDelegate=defaultDelegate
finally: unlock()
def setSharedObjectLoadingEnabled(bool):
"""
Shared E.C. is not implemented yet, hence this raises 'Unimplemented'
"""
lock()
try:
raise 'Unimplemented', 'Unsupported yet'
finally: unlock()
def handleNotification(aNotification):
"""
Registered at module's initialization time as an observer for the
'ObjectStoreCoordinator.CooperatingObjectStoreNeededNotification'.
The userInfo of that notification is a dictionary with only one key,
whose corresponding value responds to the 'entityName()' message ; the model
corresponding to that entity is searched within the default ModelSet, and it
is then used to initialize the Database object to be associated with the
newly created DatabaseContext. At last, that DatabaseContext is added to
the list of cooperatingObjectStores in the 'ObjectStoreCoordinator' which
sent this notification.
"""
lock()
try:
trace('DatabaseContext was notified')
notification_name=aNotification.name()
if notification_name == CooperatingObjectStoreNeededNotification:
trace('DatabaseContext got Coop.Obj.StoreNeeded')
objStoreCoordinator=aNotification.object()
# the userInfo dictionary for this notification is assumed to hold only
# one row
entity_name=aNotification.userInfo().get('entityName') \
or aNotification.userInfo().values()[0].entityName()
# TBD: fix docstring and doc. itself
from ModelSet import defaultModelSet
model=defaultModelSet().modelForEntityNamed(entity_name)
if model is None:
raise RuntimeError, \
'Unable to find a model for entity: %s'%entity_name
from Database import databaseForModel
database=databaseForModel(model)
dbContext=contextClassToRegister()(database)
objStoreCoordinator.addCooperatingObjectStore(dbContext)
dbContext._coordinator=objStoreCoordinator
else:
error('DatabaseContext module got an unhandled notification: %s'%notification_name)
finally:
unlock()
# Registers handleNotification as an observer
NC.addObserver(None,
handleNotification,
CooperatingObjectStoreNeededNotification )
#
class DatabaseContext(CooperatingObjectStore):
"""
"""
def __init__(self, aDatabase):
"""
Initialization also creates it own AdaptorContext to work with, by sending
to aDatabase.adaptor() the message createAdaptorContext().
Parameter 'aDatabase' should be a 'Modeling.Database' object
"""
aDatabase.registerContext(self)
self._database=aDatabase
self._adaptorContext=aDatabase.adaptor().createAdaptorContext()
self._delegate=defaultDelegate() ## delegate interface TBD
self._coordinator=None
self._channels=[]
self._instanceLock=RLock()
### Next variables are for use during the process of saving changes
### see prepareForSaveWithCoordinator()
# updateStrategy
self.__updateStrategy=UPDATE_WITH_NO_LOCKING
# the EC on behalf of which the changes should be saved
self._editingContext=None
# GlobalIDs we are responsible for
self._inserted_gIDs=[]
self._updated_gIDs=[]
self._deleted_gIDs=[]
# PKs for newly inserted objects
# See AdaptorChannel.primaryKeysForNewRowsWithEntity()
self._pks_for_inserted_gIDs={} # gID -> PK value(s)
# Set of DatabaseOperation
self._dbOperations=[]
self._pending_dbOperations={} # gid -> DatabaseOperation
# local cache for snapshots, initialized in prepareForSaveWithCoordinator
self.__snapshots=None
def adaptorContext(self):
"""
Returns the underlying AdaptorContext. It is determined at initialization
time and cannot change since then.
"""
return self._adaptorContext
def availableChannel(self):
"""
Returns an available DatabaseChannel, i.e. one in its registeredChannels()
which is not busy (==has no fetch in progress). If all registeredChannels
are busy or if the DatabaseContext has no registered channels, it posts
the notification 'DatabaseChannelNeededNotification' ; if it can find an
available channel afterwards, it simply returns it. Otherwise, a new
DatabaseChannel is created, registered and returned.
See also:
DatabaseChannel.isFetchInProgress()
"""
self.lock()
try:
for channel in self._channels:
if not channel.isFetchInProgress():
return channel
NC.postNotification(DatabaseChannelNeededNotification, self)
for channel in self._channels:
if not channel.isFetchInProgress():
return channel
# None registered, let's create one
channel=DatabaseChannel(self)
self.registerChannel(channel)
return channel
finally:
self.unlock()
def batchFetchRelationship(self, aRelationship, objects, anEditingContext):
"""
"""
#- fetch the objects / fire the faults, whatever
#- get their Gids
#- build qualifier to fetch related objects 'pk in %s'%pks
# (cf. msg Lazy Initialization Part II)
#- fetch related objects
#- now we *know* we've got them all: populate them in the db cache w/
# Database.recordSnapshotForSourceGlobalID()
#--> logically AccessArrayFaultHandler do not need to fetch the db any more
if not objects: return
ec=anEditingContext
if aRelationship.isToOne():
src_entityName=objects[0].entityName()
dst_pk_name=aRelationship.destinationAttributes()[0].name()
src_gids=[o.globalID() for o in objects]
src_fk_name=aRelationship.sourceAttributes()[0].name()
src_fks=[self.database().snapshotForGlobalID(gid)[src_fk_name]
for gid in src_gids]
from Modeling.Qualifier import KeyValueQualifier,QualifierOperatorIn
q=KeyValueQualifier(dst_pk_name,
QualifierOperatorIn,
src_fks)
dst_objs=ec.fetch(aRelationship.destinationEntityName(), q)
else: # to many
src_entityName=objects[0].entityName()
dst_attr_name=aRelationship.destinationAttributes()[0].name()
src_gids=[o.globalID() for o in objects]
src_pk_name=aRelationship.sourceAttributes()[0].name()
src_pks=[gid.keyValues()[src_pk_name] for gid in src_gids]
inv_rel=aRelationship.inverseRelationship()
if inv_rel is None: # TBD: anyInverseRelationship
raise 'aRelationship should have an inverse'
from Modeling.Qualifier import KeyValueQualifier,QualifierOperatorIn
q=KeyValueQualifier(inv_rel.name()+'.'+src_pk_name,
QualifierOperatorIn,
src_pks)
dst_objs=ec.fetch(aRelationship.destinationEntityName(), q)
db=self.database()
d={} #key: srcGlobalID, rel: [list of rel_objs]
for dst_obj in dst_objs:
dst_gid=dst_obj.globalID()
snap=db.snapshotForGlobalID(dst_gid)
src_gid=KeyGlobalID(src_entityName, {src_pk_name: snap[dst_attr_name]})
l=d.setdefault(src_gid, [])
l.append(dst_gid)
rel_name=aRelationship.name()
for src_gid in d.keys():
db.recordSnapshotForSourceGlobalID(d[src_gid], src_gid, rel_name)
def database(self):
"""
returns Database object bound to this databaseContext
"""
return self._database
def delegate(self):
"""
Returns the delegate for this object.
"""
self.lock()
try:
return self._delegate
finally:
self.unlock()
def dispose(self):
"""
Breaks the reference cycle that exists between 'self' and its 'Database'
object.
"""
self._database.unregisterContext(self)
self._database=None
def editingContextDidForgetObjectWithGlobalID(self,
anEditingContext, aGlobalID):
"""
"""
self._database.decrementSnapshotCountForGlobalID(aGlobalID)
def forgetAllLocks(self):
"""
Unimplemented
"""
self.__unimplemented__()
def forgetLocksForObjectsWithGlobalIDs(self, gIDs):
"""
Unimplemented
"""
self.__unimplemented__()
def forgetSnapshotForGlobalID(self, aGlobalID):
"""
Deletes all references for aGlobalID in the local snapshot table.
Parameter:
aGlobalID -- a GlobalID instance
Raises KeyError is aGlobalID is not registered, or RuntimeError if no
process of saving changes is in progress
"""
if not self.__snapshots:
raise RuntimeError, 'No save in progress'
self.__snapshots.forgetSnapshotForGlobalID(aGlobalID)
def forgetSnapshotsForGlobalIDs(self, gIDs):
"""
Deletes all references for each GlobalID in gIDs in the local snapshot
table.
Parameter:
gIDs -- a sequence of GlobalID instances
Raises KeyError is one GlobalID in 'gIDs' is not registered, or
RuntimeError if no process of saving changes is in progress
"""
if not self.__snapshots:
raise RuntimeError, 'No save in progress'
self.__snapshots.forgetSnapshotForGlobalIDs(gIDs)
def handleDroppedConnection(self):
"""
Unimplemented
"""
self.__unimplemented__()
def hasBusyChannels(self):
"""
Returns 1 (true) if any of the channels() answers true to the message
'isFetchInProgress()', false otherwise.
See also: DatabaseChannel.isFetchInProgress()
"""
self.lock()
try:
for channel in self._channels:
if channel.isFetchInProgress():
return 1
return 0
finally:
self.unlock()
def localSnapshotForGlobalID(self, aGlobalID):
"""
Returns the snapshot locally registered for the supplied GlobalID.
If no snapshot was registered for that name and GlobalID, or if no process
of saving changes is in progress, simply returns None.
"""
try:
return self.__snapshots.snapshotForGlobalID(aGlobalID)
except AttributeError:
return None
def localSnapshotForSourceGlobalID(self, aGlobalID, aName):
"""
Returns the toMany snapshot locally registered for the supplied GlobalID
and name.
Parameters:
aGlobalID -- a GlobalID instance
aName -- the name of the relationship
If no toMany snapshot was registered for that name and GlobalID, or
if no process of saving changes is in progress, simply returns None.
"""
try:
return self.__snapshots.snapshotForSourceGlobalID(aGlobalID, aName)
except AttributeError:
return None
def lock(self):
"""
Acquire the (reentrant) lock of the instance. Each call to 'lock()' must
be balanced with a call to 'unlock()'.
See also: unlock()
"""
self._instanceLock.acquire()
def missingObjectGlobalIDs(self):
"""
Unimplemented yet
"""
self.__unimplemented__()
def recordSnapshotForGlobalID(self, snapshot, aGlobalID):
"""
See SnapshotsHandling/recordSnapshotForGlobalID()
"""
return self.__snapshots.recordSnapshotForGlobalID(snapshot, aGlobalID)
def recordSnapshotForSourceGlobalID(self, gids, aGlobalID, aName):
"""
See SnapshotsHandling.recordSnapshotForSourceGlobalID()
"""
return self.__snapshots.recordSnapshotForSourceGlobalID(aGlobalID)
def recordSnapshots(self, snapshots):
"""
See SnapshotsHandling.recordSnapshots()
"""
return self.__snapshots.recordSnapshots(snapshots)
def recordToManySnapshots(self, snapshots):
"""
See SnapshotsHandling.recordToManySnapshots()
"""
return self.__snapshots.recordToManySnapshots(snapshots)
def recordUpdateForObject(self, aDatabaseObject, changes):
"""
Called by the ObjectStoreCoordinator when an other DatabaseContext
(including self) needs to forward changes on a given object --this can
happen, for example, when a toMany relationship that has no inverse to-one
relationship is modified in a given object 'obj1', because in this case
the modification implies the modification of a foreign key stored in one
or more objects 'obj<i>!=obj1'.
This method is for internal use, you should not need to call it by
hand. The following explanations are implementation details.
DatabaseContexts are designed in such a way that it can receive such
messages before or after recordChangesInEditingContext() has processed its
own changes. More precisely, when calling this method on self,
recordChangesInEditingContext() takes care to process all its changes
(inserted, updated or deleted objects) before it may call this method.
As a consequence:
- if recordChangesInEditingContext() has not been called yet, it just
stores the pending changes to be applied, by simply storing 'changes'
in a pending database operation's newRow(). These changes will be then
processed when recordChangesInEditingContext() is called.
- otherwise, the behaviour depends on whether the method
recordChangesInEditingContext() already created a database operation
for the object. If it did, then this database operation is updated
according to the supplied changes; otherwise a brand new database
oeration is created and local snapshots are registered for the object.
Parameters:
aDatabaseObject -- the database object for which changes are notified
changes -- a dictionary of {attributeName: value}
See also: ObjectStoreCoordinator.forwardUpdateForObject()
recordChangesInEditingContext()
"""
#self.__unimplemented_()
# search for DB OP TBD
original_gid=object_gid=aDatabaseObject.globalID()
if object_gid.isTemporary():
object_gid=self.__temporaryGID_to_KeyGlobalID[object_gid]
trace('recordUpdateForObject() aDatabaseObject: %s gid: %s changes: %s'%(aDatabaseObject, object_gid, changes))
dbOp=None
if not self._dbOperations:
# recordChangesInEditingContext() not called yet: just store changes
# for later use
trace('recordUpdateForObject() CALLED BEFORE recordChangesInEditingContext()')
entity=self._database.entityNamed(object_gid.entityName())
dbOp=DatabaseOperation(object_gid, aDatabaseObject, entity)
dbOp.setDatabaseOperator(DATABASE_UPDATE_OPERATOR)
dbOp.setNewRow(changes)
pendingDBops=self._pending_dbOperations.setdefault(object_gid, [])
pendingDBops.append(dbOp)
return
# from this point on, we know recordChangesInEditingContext() was called:
# we then either find the relevant dbOperation, or create our own
#
for op in self._dbOperations: # TBD optimize / filter et/ou acces par dict.
if op.globalID() == object_gid:
trace('recordUpdate() Found dbOp %s'%op)
dbOp=op
break
if dbOp is None:
trace('recordUpdate() Creating dbOp')
entity=self._database.entityNamed(object_gid.entityName())
dbOp=DatabaseOperation(object_gid, aDatabaseObject, entity)
dbOp.setDatabaseOperator(DATABASE_UPDATE_OPERATOR)
self._dbOperations.append(dbOp)
toManyKeys=aDatabaseObject.toManyRelationshipKeys()
snapshot=aDatabaseObject.snapshot()
snapshot.update(changes)
snapshot, toManySnapshots=\
self._objectSnapshotToDBOperatorSnapshots(snapshot, object_gid,
toManyKeys, entity)
self.recordSnapshotForGlobalID(snapshot, object_gid)
self.recordToManySnapshots({object_gid: toManySnapshots})
dbOp.setNewRow(snapshot)
trace('recordUpdate snapshot:%s dbOp.newRow():%s'%(snapshot, dbOp.newRow()))
dbOp.recordToManySnapshots(toManySnapshots)
if aDatabaseObject.globalID().isTemporary():
dbOp.setDBSnapshot({})
else:
dbOp.setDBSnapshot(self.database().snapshotForGlobalID(object_gid))
else:
dbOp.newRow().update(changes)
trace('YYYYYYYYYYYYYYYY %s %s'%(object_gid,self.snapshotForGlobalID(object_gid)))
#self.recordSnapshotForGlobalID(dbOp.newRow(), object_gid)
new_row=dbOp.newRow()
trace('recordUpdateForObject(): new_row: %s'%new_row)
def registerChannel(self, aDBchannel):
"""
Adds the supplied DatabaseChannel to the list of registeredChannels().
Raises ValueError if aDBchannel.databaseContext() is not self.
If aDBChannel is already registered it silently returns.
See also: registeredChannels(), unregisterChannel()
"""
self.lock()
try:
if aDBchannel.databaseContext() is not self:
raise ValueError, 'Tried to register a DatabaseChannel whose parent '\
'DatabaseContext is not self'
if aDBchannel not in self._channels:
self._channels.append(aDBchannel)
finally:
self.unlock()
def registeredChannels(self):
"""
Return the list of registered DatabaseChannels.
See also: registerChannels(), unregisterChannel()
"""
self.lock()
try:
return tuple(self._channels)
finally:
self.unlock()
def registerLockedObjectWithGlobalID(self, aGlobalID):
"""
Unimplemented
"""
def setDelegate(self, aDelegate):
"""
Sets the object's delegate (not enabled yet)
"""
self.lock()
try:
self._delegate=aDelegate
finally:
self.unlock()
def setUpdateStrategy(self, aStrategy):
"""
Sets the DatabaseContext's update strategy
Raise RuntimeError if the DatabaseContext is currently saving changes.
"""
self.__unimplemented__()
def snapshotForGlobalID(self, aGlobalID, timestamp=DistantPastTimeInterval):
"""
Returns the snapshot for 'aGlobalID'. Snapshots are searched within the
local set of snapshots, then in the object's database.
Returned value is 'None' if no snapshot was recorded for 'aGlobalID' or
if a snapshot exists with a smaller timestamp than parameter 'timestamp'
"""
snapshot=self.localSnapshotForGlobalID(aGlobalID)
if snapshot is None:
snapshot=self.database().snapshotForGlobalID(aGlobalID, timestamp)
return snapshot
def snapshotForSourceGlobalID(self, aGlobalID, relationshipName,
timestamp=DistantPastTimeInterval):
"""
Returns the snapshot for 'aGlobalID' and the toMany relationship
'relationshipName'. Snapshots are searched within the local set
of snapshots, then in the object's database.
Returned value is 'None' if no snapshot was recorded for 'aGlobalID' and
'relationshipName' or if a snapshot exists with a smaller timestamp than
parameter 'timestamp'
"""
snapshot=self.localSnapshotForSourceGlobalID(aGlobalID, aRelationshipName)
if not snapshot:
snapshot=self.database().snapshotForSourceGlobalID(aGlobalID, relationshipName, timestamp)
return snapshot
def unlock(self):
"""
Releases the (reentrant) lock of the instance.
See also: lock()
"""
self._instanceLock.release()
def unregisterChannel(self, aDBChannel):
"""
Unregister 'aDBChannel' from the list of registered DatabaseChannels.
See also: registerChannel(), registeredChannels()
"""
self.lock()
try:
self._channels.remove(aDBChannel)
finally:
self.unlock()
def updateStrategy(self):
"""
Returns the DatabaseContext's updateStrategy
Default is UPDATE_WITH_NO_LOCKING
"""
return self.__updateStrategy
##
## CooperatingObjectStore API
##
def commitChanges(self):
"""
Commits the transaction in progress --that transaction was opened when the
ObjectStore got one of the two message 'saveChangesInEditingContext()' or
'performChanges()'.
This is the penultimate step of save changes.
Raises 'RuntimeError' if the DatabaseContext is not saving changes.
See also: saveChangesInEditingContext(),
prepareForSaveWithCoordinator(), recordChangesInEditingContext(),
performChanges(),
rollbackChanges()
finalizeCommitChanges()
"""
if not self.isSavingChanges():
raise RuntimeError, "Invalid state: DatabaseContext isn't saving changes"
# Commit DB transaction
self.__channel.adaptorContext().commitTransaction()
# Note: _endTransaction() is NOT called here, but at the end of
# finalizeCommitChanges()
def coordinator(self):
"""
Returns the current ObjectStoreCoordinator which which the DatabaseContext
cooperates during the process of saving changes. The coordinator is set
when prepareForSaveWithCoordinator() is called, and it is reset whenever
the process aborts --either with commitChanges() or rollbackChanges().
Outside a process of save changes, this returns None.
See also: prepareForSaveWithCoordinator(), commitChanges(),
rollbackChanges(), _endTransaction()
"""
return self._coordinator
def finalizeCommitChanges(self):
"""
Called by the ObjectStoreCoordinator to re-synchronize the local snapshots
with the (application-wide) Database's snapshots and to clean everything
up.
This is the last step of saving changes. DatabaseContext's implementation
forwards all changes made to its Database then empties/invalidates the
internal state used during the process of saving changes. Last, it
forwards the new KeyGlobalIDs (which replace the TemporaryGlobalIDs
initially assigned to inserted objects) to the EditingContext.
You should never called this by yourself, it is automatically called by
the framework.
"""
if not self.isSavingChanges():
raise RuntimeError, "Invalid state: DatabaseContext isn't saving changes"
# Forwards snapshot changes to Database
self.database().forgetSnapshotsForGlobalIDs(self._deleted_gIDs)
self.database().setTimestampToNow()
self.database().recordSnapshots(self.__snapshots.snapshots())
self.database().recordToManySnapshots(self.__snapshots.toManySnapshots())
#trace('_inserted_gIDs: %s'%self._inserted_gIDs)
#trace('_pks_for_inserted_gIDs: %s'%self._pks_for_inserted_gIDs)
for gID in self._inserted_gIDs:
obj=self._editingContext.objectForGlobalID(gID)
keyGID=self.__temporaryGID_to_KeyGlobalID[gID]
debug('finalizeCommitChanges %s %s->%s'%(obj,gID,keyGID))
NC.postNotification(GlobalIDChangedNotification, gID, {gID: keyGID})
# Cleans up temporary state
self._endTransaction()
def handlesFetchSpecification(self, aFetchSpecification):
"""
Tells whether the DatabaseContext is responsible for the fetch
specification. A DatabaseContext is responsible for a fetch specification
if the fetchSpec's entity is registered in the entities set managed by the
dbContext.database()
"""
if self._database.entityNamed(aFetchSpecification.entityName()):
return 1
return 0
def ownsEntityName(self, entityName):
"""
Tells whether the DatabaseContext is responsible for the objects of the
supplied entity. Namely: if the entity is registered in the entities set
managed by the dbContext.database()
"""
try:
# will fail if aGlobalID has no method entityName (TemporaryGlobalID)
if self._database.entityNamed(entityName):
return 1
except:
pass
return 0
def ownsGlobalID(self, aGlobalID):
"""
Tells whether the DatabaseContext is responsible for the object. A
DatabaseContext is responsible for an object if the global id's entity is
registered in the entities set managed by the dbContext.database()
"""
try:
# will fail if aGlobalID has no method entityName (TemporaryGlobalID)
if self._database.entityNamed(aGlobalID.entityName()):
return 1
except:
pass
return 0
def ownsObject(self, anObject):
"""
Tells whether the DatabaseContext is responsible for the object. A
DatabaseContext is responsible for an object if the object's entity is
registered in the entities set managed by the dbContext.database()
"""
cd=ClassDescription.classDescriptionForObject(anObject)
if self._database.entityNamed(cd.entityName()):
return 1
return 0
def performChanges(self):
"""
This is the third and last step (apart from commitChanges() or
rollbackChanges()) being triggered by the ObjectStoreCoordinator when the
latter was instructed to save the changes made in an EditingContext.
The 'EditingContext' instance this method works with is the one supplied
when 'prepareForSaveWithCoordinator()' was called.
It depends on prepareForSaveWithCoordinator() and
recordChangesInEditingContext() having been called before ; you should
never need to call this method by hand.
What we do here is derive AdaptorOperations from the DatabaseOperations
built on the previous step [recordChangesInEditingContext()]. These
AdaptorOperations are ordered and sent to an available AdaptorChannel
as the parameter for message performAdaptorOperation()
Raises 'RuntimeError' if the DatabaseContext is not saving changes.
"""
if not self.isSavingChanges():
raise RuntimeError, "Invalid state: DatabaseContext isn't saving changes"
self.lock()
try:
self.__channel=self.availableChannel().adaptorChannel()
if self.__channel is None:
raise RuntimeError, 'Unable to find an available AdaptorChannel'
self.__channel.adaptorContext().beginTransaction()
_adaptorOperations=[]
trace('performChanges: dbOps: %s'%self._dbOperations)
#import pprint
#pprint.pprint(self._dbOperations)
for dbOp in self._dbOperations:
# Simple: No locking, just operations
_adOps = self.__databaseOperationToAdaptorOperations(dbOp)
if not _adOps:
pass
# Note: the following statement was initially coded:
#self.forgetSnapshotForGlobalID(dbOp.globalID())
# meaning: if the object was marked as updated but, in fact, no changes
# needs to be forwarded to the database, then we simply discard the
# update and forget the snapshots. Seems Ok...
# BUT: this leads to a buggy behaviour when an object has been marked
# as updated because at least one of its toMany-relationships-related
# objects as been removed (and possibly deleted): sure, since the
# information about that dropped relationship is stored in the
# destination object (in the FK), there is no changes to forward to
# the database as far as the source object (=self) is concerned.
# However, if we forget the snapshots about that object, the
# Database's snapshotForSourceGlobalID() will not be updated and will
# be out of sync.
# cf. test_EditingContext_Global.test_16_toManySnapshotsUpdated()
else:
_adaptorOperations += _adOps
# order dbOperations NB: __TBD delegate
_adaptorOperations.sort()
trace("adaptorOperations: %s"%str(_adaptorOperations))
#import pdb ; pdb.set_trace()
self.__channel.performAdaptorOperations(_adaptorOperations)
finally:
self.unlock()
def __databaseOperationToAdaptorOperations(self, databaseOperation):
"""
Internally used to build a list of AdaptorOperations from a
DatabaseOperation.
"""
dbOperator=databaseOperation.databaseOperator()
if dbOperator is DATABASE_NOTHING_OPERATOR:
return []
elif dbOperator is DATABASE_INSERT_OPERATOR:
adOp=AdaptorOperation(databaseOperation.entity(),
ADAPTOR_INSERT_OPERATOR)
adOp.setChangedValues(databaseOperation.newRow())
trace('dbOptoAdOp: new row: %s'%databaseOperation.newRow())
return [adOp]
elif dbOperator is DATABASE_DELETE_OPERATOR:
adOp=AdaptorOperation(databaseOperation.entity(),
ADAPTOR_DELETE_OPERATOR)
entity=databaseOperation.entity()
gID=databaseOperation.globalID()
qual=entity.qualifierForPrimaryKey(gID.keyValues())
adOp.setQualifier(qual)
return [adOp]
elif dbOperator is DATABASE_UPDATE_OPERATOR:
rowDiffs=databaseOperation.rowDiffs()
if not rowDiffs:
trace('NO ROW DIFF %s'%databaseOperation)
return []
adOp=AdaptorOperation(databaseOperation.entity(),
ADAPTOR_UPDATE_OPERATOR)
entity=databaseOperation.entity()
adOp.setChangedValues(rowDiffs)
gID=databaseOperation.globalID()
qual=entity.qualifierForPrimaryKey(gID.keyValues())
adOp.setQualifier(qual)
return [adOp]
else:
raise ValueError, 'Unknown databaseOperation.databaseOperator(): %s'%str(dbOperator)
def prepareForSaveWithCoordinator(self, aCoordinator, anEditingContext):
"""
This is the first step being triggered by the ObjectStoreCoordinator when
it is instructed to save the changes made in an EditingContext.
See also: recordChangesInEditingContext(), performChanges(),
commitChanges(), rollbackChanges(),
EditingContext.saveChanges()
(Copied from CooperatingObjectStore interface)
DatabaseContext's implementation examines objects in 'anEditingContext' ;
all inserted, updated or deleted objects that are under its control
(cf. ownsGlobalID()) are kept internally --until the process ends with
either commitChanges() or rollbackChanges(). The DatabaseContext
afterwards cooperates with an available AdaptorChannel to generate
primary keys for newly inserted objects.
Raises 'RuntimeError' if the DatabaseContext is not saving changes.
Parameters:
aCoordinator -- the ObjectStoreCoordinator which called the method.
anEditingContext -- the EditingContext asking for its changes to be
saved.
Conformance to the CooperatingObjectStore interface.
See also: coordinator()
"""
self.__snapshots=DatabaseContext.SnapshotsTable()
self._coordinator=aCoordinator
self._editingContext=ec=anEditingContext
self.__temporaryGID_to_KeyGlobalID={}
# first, extract from the EditingContext the objects that we
# handle here
for gID in ec.insertedGlobalIDs():
# NB: cannot use ownsGlobalID on a TemporaryGlobalID
if self.ownsObject(ec.objectForGlobalID(gID)):
self._inserted_gIDs.append(gID)
for gID in ec.updatedGlobalIDs():
if self.ownsGlobalID(gID):
self._updated_gIDs.append(gID)
for gID in ec.deletedGlobalIDs():
if self.ownsGlobalID(gID):
self._deleted_gIDs.append(gID)
# get the primary keys for inserted objects
# this could be made more efficient if PKs were asked within a single
# round-trip to the DB
self.lock()
try:
channel=self.availableChannel().adaptorChannel()
for gID in self._inserted_gIDs:
entity=ec.objectForGlobalID(gID).classDescription().entity()
pk=channel.primaryKeysForNewRowsWithEntity(1, entity)
self._pks_for_inserted_gIDs[gID]=pk[0]
newGid=KeyGlobalID(entity.name(), self._pks_for_inserted_gIDs[gID])
self.__temporaryGID_to_KeyGlobalID[gID]=newGid
finally:
self.unlock()
def _recordChangesInEditingContext(self, gID, obj, dbOperator):
"""
Called by recordChangesInEditingContext() for each inserted, updated and
deleted object it examines. This method is for internal use only, you
should not need to call it by hand.
This method creates the corresponding DatabaseOperation for the object,
adds it to the DatabaseContext's database operations. It also registers
the local snapshots for that objects (whose local snapshots will be passed
to the database() obect when changes are saved). It also examines any
pending DatabaseOperations that have been received by
recordUpdateForObject() before recordChangesInEditingContext() was called
(these changes were submitted by other DatabaseContexts). Last, it also
examines the resulting DatabaseOperation and set its operator to
DATABASE_NOTHING_OPERATOR if appropriate.
Parameters:
gID -- the globalID of the object, either temporary or not. If it is
temporary, it will be translated into the non-temporary GlobalID
the object will receive after it's been changed.
obj -- the object to register
dbOperator -- the database operator to be used for the created
DatabaseOperation
See also: _examineToManySnapshots()
"""
entity=self.database().entityForObject(obj)
if dbOperator==DATABASE_INSERT_OPERATOR or gID.isTemporary():
# Build and use a regular (Key)GlobalID
gID=KeyGlobalID(entity.name(), self._pks_for_inserted_gIDs[gID])
else:
pass
#self.__temporaryGID_to_KeyGlobalID[tempGID]=gID
dbOp=DatabaseOperation(gID, obj, entity)
dbOp.setDatabaseOperator(dbOperator)
self._dbOperations.append(dbOp)
# Now for the snapshotting part
objSnapshot=obj.snapshot()
toManyKeys=obj.toManyRelationshipKeys()
snapshot, toManySnapshots=\
self._objectSnapshotToDBOperatorSnapshots(objSnapshot, gID,
toManyKeys, entity)
# get pending operations received by recordUpdateForObject()
pending_ops=self._pending_dbOperations.get(gID, [])
for pending_op in pending_ops:
snapshot.update(pending_op.newRow())
if pending_ops:
del self._pending_dbOperations[gID]
self.recordSnapshotForGlobalID(snapshot, gID)
self.recordToManySnapshots({gID: toManySnapshots})
dbOp.setNewRow(snapshot)
dbOp.recordToManySnapshots(toManySnapshots)
if dbOperator==DATABASE_INSERT_OPERATOR:
dbOp.setDBSnapshot({})
else:
dbOp.setDBSnapshot(self.database().snapshotForGlobalID(gID))
if dbOperator==DATABASE_UPDATE_OPERATOR:
# requires:
# dbOp.__dbSnapshot/setDBSnapshot + .__newRow/setNewRow
rowDiffs=dbOp.rowDiffs()
if not rowDiffs:
trace('recordChangesInEditingContext() ->NOTHING obj:%s gID:%s'%(obj, gID))
dbOp.setDatabaseOperator(DATABASE_NOTHING_OPERATOR)
trace('_recordChangesInEditingContext returning: %s, %s'%(gID, toManySnapshots))
return gID, toManySnapshots
def recordChangesInEditingContext(self):
"""
Second step being triggered by the ObjectStoreCoordinator when the latter
was instructed to save the changes made in an EditingContext.
The 'EditingContext' instance this method works with is the one supplied
when 'prepareForSaveWithCoordinator()' was called.
This method has two distinct tasks:
- it builds the set of DatabaseOperations needed to make the changes
persistent,
- it also registers every changed objects into the local snapshotting
table. **IMPORTANT**: the local snapshotting mechanism does not have
any references to TemporaryGlobalID objects: new objects are detected
and a brand new GlobalID is built for them, based upon the PKs values
that were computed at the previous step
(cf. prepareForSaveWithCoordinator()). This applies to ``regular''
snapshots and to ``toMany snapshots'' as well.
You should never need to call this method by yourself, it is automatically
called by the ObjectStoreCoordinator in charge.
See also: prepareForSaveWithCoordinator(), performChanges(),
commitChanges(), rollbackChanges(),
EditingContext.saveChanges()
recordUpdateForObject()
Raises 'RuntimeError' if the DatabaseContext is not saving changes.
(Copied from CooperatingObjectStore interface)
DatabaseContext implementation builds all necessary DatabaseOperation
objects for the observed changes. These DatabaseOperations are then
examined to check whether they do not imply changes to other objects in
relation with the one they were build for, and if it is the case, the
DatabaseContext forwards these changes to the ObjectStoreCoordinator.
You can also refer to DatabaseOperation 's documentation for a more
complete discussion on this topic.
This method also always add a fake DatabaseOperation to its own set, whose
operator is DATABASE_NOTHING_OPERATOR and whose globalID, object and
entity all equals to the string 'fake' --this is used by
recordUpdateForObject(), see this method for a complete discussion on
this.
See also: DatabaseOperation,
_recordChangesInEditingContext(), _examineToManySnapshots()
"""
if not self.isSavingChanges():
raise RuntimeError, "Invalid state: DatabaseContext isn't saving changes"
# recordUpdateForObject() uses self._dbOperations to check whether
# we've been already called, so we add a fake dbOperation making NOTHING
# See recordUpdateForObject() docstring for details
fake_dbOp=DatabaseOperation('fake', 'fake', 'fake')
fake_dbOp.setDatabaseOperator(DATABASE_NOTHING_OPERATOR)
self._dbOperations.append(fake_dbOp)
# this holds tomany snapshots, returned by _recordChangesInEditingContext()
# for use by the last phase, when invoking _examineToManySnapshots()
toManySnaps={}
for tempGID in self._inserted_gIDs:
obj=self._editingContext.objectForGlobalID(tempGID)
toManySnaps[tempGID]=self._recordChangesInEditingContext(tempGID, obj, DATABASE_INSERT_OPERATOR)
for gID in self._updated_gIDs:
obj=self._editingContext.objectForGlobalID(gID)
toManySnaps[gID]=self._recordChangesInEditingContext(gID, obj, DATABASE_UPDATE_OPERATOR)
for gID in self._deleted_gIDs:
obj=self._editingContext.objectForGlobalID(gID)
toManySnaps[gID]=self._recordChangesInEditingContext(gID, obj, DATABASE_DELETE_OPERATOR)
# process the remaining pending databaseOperation
for pending_gid, pending_dbOps in self._pending_dbOperations.items():
trace('recordUpdate(): processing pending gid: %s'%pending_gid)
assert(not pending_gid.isTemporary()) ##
obj=self._editingContext.objectForGlobalID(gid)
toManySnaps[gID]=self._recordChangesInEditingContext(pending_gid, obj, DATABASE_UPDATE_OPERATOR)
self._updated_gIDs.append(pending_gid)
assert(self._pending_dbOperations == {}) # reset
# Forwards possible changes (to recordUpdateForObject()) at the end
for tempGID in self._inserted_gIDs:
obj=self._editingContext.objectForGlobalID(tempGID)
self._examineToManySnapshots(obj,
toManySnaps[tempGID][0],
toManySnaps[tempGID][1])
for gID in self._updated_gIDs:
obj=self._editingContext.objectForGlobalID(gID)
self._examineToManySnapshots(obj, gID, toManySnaps[gID][1])
for gID in self._deleted_gIDs:
obj=self._editingContext.objectForGlobalID(gID)
self._examineToManySnapshots(obj, gID, toManySnaps[gID][1])
def _examineToManySnapshots(self, object, aGlobalID, toManySnapshots):
"""
Examine the toMany snapshot computed by recordChangesInEditingContext()
for an object, and takes the responsability to invoke
ObjectStoreCoordinator.forwardUpdateForObject() for any object that should
be notified of changes they couldn't be aware of yet. This is the case
when an object modifies a to-many relationship which has no inverse: in
this case we have to notify the added or removed objects that the foreign
key storing the information for the to-many rel. should be changed.
This method is for internal use, you should not need to call it by hand.
Note: this method does NOT react to any modifications of to-many
relationships which have an inverse to-one relationship defined in the
model. It is the developer's responsability to maintain the consistency of
the graph of objects hold by an EditingContext; when this is done, as
expected, the added or removed objects have already been notified that
something has changed and shouldn't be notified twice.
Parameters:
object -- the object to which the toManySnapshots belongs
aGlobalID -- the object's non-temporary global id
toManySnapshots -- the to-many snapshots to be examined
"""
changes={}
ec=self._editingContext
for key in toManySnapshots.keys():
rel=object.classDescription().entity().relationshipNamed(key)
assert(rel.isToMany())
# to-many rel
if rel.inverseRelationship():
continue
db_snap=self.database().snapshotForSourceGlobalID(aGlobalID, key)
if list(db_snap) == toManySnapshots[key]:
continue
trace('############### %s != %s'%(list(db_snap), toManySnapshots[key]))
inv_rel=rel.anyInverseRelationship()
added_gids=[gid for gid in toManySnapshots[key] if gid not in db_snap]
removed_gids=[gid for gid in db_snap if gid not in toManySnapshots[key]]
trace('ADDED: %s'% added_gids)
trace('REMOVED: %s'% removed_gids)
modified_gids=added_gids+removed_gids
###
def forwardChangesToObjects(self, inv_rel, removed):
for join in inv_rel.joins():
src_attr=join.sourceAttributeName()
dst_attr=join.destinationAttributeName()
if removed:
changes[src_attr]=None
else:
changes[src_attr]=aGlobalID.keyValues()[dst_attr]
real_gid=gid
for g in self.__temporaryGID_to_KeyGlobalID.keys():
if self.__temporaryGID_to_KeyGlobalID[g]==gid:
real_gid=g
break
related_object=ec.faultForGlobalID(real_gid, ec)
trace('_examineToManySnapshots() related object: %s real_gid: %s'%(related_object, real_gid))
self._coordinator.forwardUpdateForObject(related_object, changes)
###
for gid in added_gids:
forwardChangesToObjects(self, inv_rel, removed=0)
for gid in removed_gids:
forwardChangesToObjects(self, inv_rel, removed=1)
def _objectSnapshotToDBOperatorSnapshots(self, objectSnapshot,
globalID, toManyKeys, entity):
"""
Internally used to convert a CustomObject.snapshot() to a snapshot a
DatabaseOperator can fed with.
Returns a tuple of two elements:
- the snapshot, as described in DatabaseOperation.setNewRow() ; if
globalID.isTemporary() evaluates to false, the returned snapshot gets
the primary key(s) as well, extracted from the object's globalID or,
for new objects, from the PKs generated during
prepareForSaveWithCoordinator().
- the toMany snapshots, as described in
DatabaseOperation.recordToManySnapshots()
In conformance to what is stated in recordToManySnapshots(), this method
examines all toMany snapshots and replaces the temporary GlobalIDs, if
any, with the corresponding ``regular'' KeyGlobalID.
"""
toManySnap={}
snap=objectSnapshot.copy()
for key in toManyKeys:
_sn=objectSnapshot[key]
from CustomObject import Snapshot_ToManyFault
if not isinstance(_sn, Snapshot_ToManyFault):
# 'faulted toMany' are instances of Snapshot_ToManyFault when returned
# by CustomObject.snapshot()
toManySnap[key]=objectSnapshot[key]
# replace any TemporaryGlobalID with its corresponding new value
toManySnap_key=tuple(toManySnap[key])
for temp_gid in toManySnap_key:
if temp_gid.isTemporary():
toManySnap[key].remove(temp_gid)
toManySnap[key].append(self.__temporaryGID_to_KeyGlobalID[temp_gid])
trace('Mapping %s to %s'%(str(temp_gid), str(self.__temporaryGID_to_KeyGlobalID[temp_gid])))
del snap[key]
#toManySnap={globalID: toManySnap}
if not globalID.isTemporary(): # __TBD remove this test (normally useless)
snap.update(globalID.keyValues())
else:
raise RuntimeError, 'OOOOPS, shouldnt happen!!'
#snap.update(self._pks_for_inserted_gIDs[globalID])
# Remove keys corresponding to a to-one relationship and
# replace them with the appropriate (source-)attributes' keys and values
for key in snap.keys():
rel=entity.relationshipNamed(key)
if rel and rel.isToOne():
gID=snap[key]
del snap[key]
d={}
srcAttrs=rel.sourceAttributes()
srcAttrNames=[attr.name() for attr in srcAttrs]
pkValues={}
if gID is None:
for attr in srcAttrs:
destinationName=rel.destinationAttributeForSourceAttribute(attr).name()
pkValues[destinationName]=None
elif gID.isTemporary():
# Replace with the newly assigned one
pkValues=self._pks_for_inserted_gIDs[gID]
else:
pkValues=gID.keyValues()
#import pdb ; pdb.set_trace()
for idx in range(len(srcAttrs)):
snap[srcAttrNames[idx]]=pkValues[rel.destinationAttributeForSourceAttribute(srcAttrs[idx]).name()]
# Last, check that all FKs are included, as expected
# It can happen that it is not the case when a toMany rel. has no inverse
# --> see test_EditingContext_Global_Inheritance,
# test: test_10_toMany_with_no_inverse__db_snapshot_is_correct()
for attr in entity.attributesNames():
snap.setdefault(attr, None)
return (snap, toManySnap)
def rollbackChanges(self):
"""
Rollbacks any changes sent at the previous step (cf. performChanges()) and
cleans up the DatabaseContext's internal variables which support the
process of saving changes.
Tou should not call this method by yourself, it is automatically triggered
by the framework, when appropriate.
Raises 'RuntimeError' if the DatabaseContext is not saving changes.
See also: commitChanges()
"""
if not self.isSavingChanges():
raise RuntimeError, "Invalid state: DatabaseContext isn't saving changes"
self.__channel.adaptorContext().rollbackTransaction()
self._endTransaction()
def valuesForKeys(self, keys, aDatabaseObject):
"""
Unimplemented: see recordUpdateForObject() for details
"""
self.__unimplemented__()
def _endTransaction(self):
"""
Private method internally used by either methods finalizeCommitChanges()
(called after commitChanges()) or rollbackChanges() to empty the internal
cache (holding the EditingContext, the ObjectStoreCoordinator if any, the
generated primary keys, the list of globalIDs of objects that had changes
and the local cache for snapshots).
Note that the underlying adaptor channel that was opened and used during
performChanges is closed here (hence at the end of the transaction, either
after commit or rollback).
See also: isSavingChanges()
"""
self._coordinator=None
self._editingContext=None
self._inserted_gIDs=[]
self._updated_gIDs=[]
self._deleted_gIDs=[]
self._pks_for_inserted_gIDs={}
self._dbOperations=[]
self._pending_dbOperations={}
self.__snapshots=None
self.__channel.closeChannel()
self.__channel=None
self.__temporaryGID_to_KeyGlobalID=None
def isSavingChanges(self):
"""
Tells whether the DatabaseContext is currently saving changes.
See also: prepareForSaveWithCoordinator(), saveChangesInEditingContext(),
_endTransaction()
"""
return self.__snapshots is not None
##
## ObjectStore API
##
def arrayFaultWithSourceGlobalID(self, gID, aRelationshipName,
anEditingContext):
"""
"""
## remember: pass object to be weakreferenced by toMany fault Handler
object=anEditingContext.objectForGlobalID(gID)
return AccessArrayFaultHandler(gID, aRelationshipName,
self, anEditingContext, object)
# defined in superclass???
#def editingContextDidForgetObjectWithGlobalID(self, aContext, aGlobalID):
# """
# """
def faultForGlobalID(self, keyGlobalID, anEditingContext):
"""
Creates and registers a new Fault for the supplied globalID.
Raises 'ValueError' if keyGlobalID is invalid (i.e. it does not responds
to message 'entityName()' --typically, if it is not a 'KeyGlobalID')
Parameters:
keyGlobalID -- a 'KeyGlobalID'
anEditingContext -- the editing context in which the fault should be
registered.
"""
try:
entityName=keyGlobalID.entityName()
except AttributeError:
raise ValueError, \
"Parameter keyGlobalID does not responds to message 'entityName()'"
cd=ClassDescription.classDescriptionForName(entityName)
cd_root=cd.rootClassDescription()
root_keyGlobalID=KeyGlobalID(cd_root.entityName(), keyGlobalID.keyValues())
obj=anEditingContext.objectForGlobalID(root_keyGlobalID)
if obj:
trace('Got it:%s w/ root_keyGlobalID: %s'%(obj,root_keyGlobalID))
return obj
fault=cd_root.createInstanceWithEditingContext(anEditingContext)
faultHandler=AccessFaultHandler(root_keyGlobalID, self,
anEditingContext)
fault.turnIntoFault(faultHandler)
trace('Registering fault w/ GlobalID: %s'%str(root_keyGlobalID))
anEditingContext.recordObject(fault, root_keyGlobalID)
#trace('cd: %s / cd_root: %s'%(cd, cd_root))
# inheritance support
trace('Adding ec as an observer for GlobalIDChangedNotification & GlobalID: %s'%root_keyGlobalID)
NC.addObserver(anEditingContext, EditingContext.handleNotification,
GlobalIDChangedNotification, root_keyGlobalID)
return fault
def faultForRawRow(self, row, entityName, anEditingContext):
"""
Turns a row (dictionary) into a real object. Any row, such as the one
returned by a fetch when raw rows is activated, can be turned into a
real object given that it contains the primary keys.
Parameters:
row -- a dictionary. This dictionary should have the entity's primary
keys in its keys (and their corresponding values)
entityName -- the name of the entity the row represents
anEditingContext -- The EditingContext in which the object should be
registered. Defaults to self if None or omitted.
See also: EditingContext.fetch(), FetchSpecification.setFetchesRawRows
"""
entity=self._database.entityNamed(entityName)
pks_names=entity.primaryKeyAttributeNames()
pks={}
for pk in pks_names:
if not row.has_key(pk):
raise ValueError("Cannot convert row to object: row should at least contain entity %s's primary key(s) but pk '%s' is not there"%(entityName, pk))
pks[pk]=row[pk]
gid=KeyGlobalID(entityName, pks)
return self.faultForGlobalID(gid, anEditingContext)
def handlesObject(self, anObject):
"""
Simply invokes ownsObject() on itself.
Conformance to the ObjectStore API.
"""
return self.ownsObject(anObject)
def initializeObject(self, aDatabaseObject, aGlobalID, anEditingContext):
"""
Parameters:
aGlobalID -- KeyGlobalID (non temporary)
"""
if anEditingContext.objectForGlobalID(aGlobalID)!=aDatabaseObject:
raise ValueError, 'aDatabaseObject %s is not registered within the '\
'EditingContext %s for globalID: %s'%(repr(aDatabaseObject),
repr(anEditingContext),
repr(aGlobalID))
debug('initializeObject %s %s'%(aGlobalID, aDatabaseObject))
self.database().incrementSnapshotCountForGlobalID(aGlobalID)
entity=self._database.entityNamed(aGlobalID.entityName())
attrs=entity.classProperties_attributes()
rels=entity.classProperties_relationships()
classPropsNames=entity.classPropertiesNames()
snapshot=self._database.snapshotForGlobalID(aGlobalID)
aDatabaseObject.prepareForInitializationWithKeys(classPropsNames)
for attr in attrs:
attrName=attr.name()
value=snapshot[attrName]
if value and attr.type() == 'DateTime':
value=attr.convertStringToAttributeType(value)
aDatabaseObject.takeStoredValueForKey(value, attrName)
# faults initialization
#cd=aDatabaseObject.classDescription()
for rel in rels:
#destCD=cd.classDescriptionForDestinationKey(rel.name())
# Now we have to initialize the object's relationships
if rel.isToOne():
#import pdb ; pdb.set_trace()
destKeyValues={}
for join in rel.joins():
# This test is to make sure this will not be forgot when this
# feature will be added
# NB: for a PK to PK toOne relationship, we have no choice but
# build a toOne fault for the destination
# (which is not the case for FK-to-PK toOne relationship)
if join.sourceAttributeName() in \
rel.sourceEntity().primaryKeyAttributeNames():
raise 'Unimplemented', 'toOne relationship joining PKs to PKs is '\
'is not supported yet '
if snapshot[join.sourceAttributeName()]:
destKeyValues[join.destinationAttributeName()]=snapshot[join.sourceAttributeName()]
if destKeyValues:
faultGlobalID=KeyGlobalID(rel.destinationEntityName(),
destKeyValues)
# Maybe we already have that object?
# If true, we *must* use it (EditingContext/uniquing)
destObject=anEditingContext.objectForGlobalID(faultGlobalID)
if destObject is None:
## No such object was found within the editingContext: time to
## create a toOne fault
destObject=self.faultForGlobalID(faultGlobalID, anEditingContext)
else:
destObject=None
aDatabaseObject.takeStoredValueForKey(destObject, rel.name())
else: # toMany fault
#faultHandler=AccessArrayFaultHandler(aGlobalID, rel.name(),
# self, anEditingContext)
faultHandler=self.arrayFaultWithSourceGlobalID(aGlobalID,
rel.name(),
anEditingContext)
aDatabaseObject.takeStoredValueForKey(faultHandler, rel.name())
def invalidateAllObjects(self):
"""
Unimplemented yet
"""
raise 'Unimplemented yet'
#NC.postNotification(InvalidatedAllObjectsInStoreNotification, self)
def invalidateObjectsWithGlobalIDs(self, gIDs):
"""
Unimplemented yet.
"""
self.__unimplemented__()
def isObjectLockedWithGlobalID(self, aGlobalID, anEditingContext):
"""
Locking is not supported yet: raises 'Unimplemented'
"""
self.__unimplemented__()
def lockObjectWithGlobalID(self, aGlobalID, anEditingContext):
"""
Locking is not supported yet: raises 'Unimplemented'
"""
self.__unimplemented__()
def objectsForSourceGlobalID(self, srcGlobalID, aRelationshipName,
anEditingContext):
"""
This method is dedicated for use by a toMany fault handler.
See also: AccessArrayFaultHandler
"""
gids=self.database().snapshotForSourceGlobalID(srcGlobalID,
aRelationshipName)
ec=anEditingContext
objects=[]
# First check that the corresponding data were not already fetched
if gids:
# already fetched
trace("snapshotForSrcGlobalID(%s,%s): %s"%(str(srcGlobalID),
aRelationshipName,
str(gids)))
for gid in gids:
object=ec.objectForGlobalID(gid)
if object:
trace('E.C. already has an object for gid: %s'%gid)
objects.append(object)
else:
trace("E.C. hasn't any object for gid: %s"%gid)
object=self.faultForGlobalID(gid, ec)
# Do we need to trigger the fault? I dont think so
objects.append(object)
else:
trace("Building qualifier")
# build the qualifier
cd=ClassDescription.classDescriptionForName(srcGlobalID.entityName())
srcRel=cd.entity().relationshipNamed(aRelationshipName)
srcSnapshot=self.snapshotForGlobalID(srcGlobalID)
if not srcSnapshot:
raise GeneralDatabaseException,\
'Unable to find snapshot for globalID: %s'%str(srcGlobalID)
#qualifiers=[]
dict={}
for join in srcRel.joins():
value=srcSnapshot[join.sourceAttributeName()]
#qual=KeyValueQualifier(join.destinationAttributeName(),
# operator=QualifierOperatorEqual,
# value=value)
#qualifiers.append(qualifiers)
dict[join.destinationAttributeName()]=value
#qualifier=AndQualifier(qualifiers)
qualifier=qualifierToMatchAllValues(dict)
fetchSpec=FetchSpecification(entityName=srcRel.destinationEntityName(),
qualifier=qualifier,
deepFlag=1) # !!!
#__TBD: recordToManySnapshot
# fetch
# TBD Note: we fetch against self, i.e. a DBContext, NOT the EC
# TBD This seems reasonable, but additional testing must be done
# TBD for being sure, especially w.r.t. a toMany fault being
# TBD fired when the set of objects in relation possibly
# TBD contains an object marked for deletion.
# TBD --> See comments in test_EC_Global's
# TBD test_06_deleteRule_nullify() for details
objects=self.objectsWithFetchSpecification(fetchSpec, ec)
return objects
def objectsCountWithFetchSpecification(self, aFetchSpec, anEditingContext):
"""
Returns the approximate number of objects that would be returned by
objectsWithFetchSpecification() if called with the very same parameters.
About ``approximate'': the number returned is in fact the upper bound ; as
the objects are not actually fetched against the database, it is not
possible to determine whether some have already been deleted within
anEditingContext (see objectsWithFetchSpecification() as well).
Parameters:
aFetchSpecification -- a FetchSpecification object describing the
objects to be fetched
anEditingContext -- the EditingContext in which the objects would be
fetched. This is in fact an optional argument: since no objects are
fetched it is not really needed.
See also: DatabaseChannel.selectCountObjectsWithFetchSpecification()
"""
entitiesNames=[aFetchSpec.entityName()]
if aFetchSpec.isDeep():
import ModelSet
defaultModelSet=ModelSet.defaultModelSet()
entity=defaultModelSet.entityNamed(aFetchSpec.entityName())
entitiesNames+=entity.allSubEntitiesNames()
objects=[]
count=c=0
for entityName in entitiesNames:
aFetchSpec.setEntityName(entityName)
self.lock()
try:
channel=self.availableChannel()
c=channel.selectCountObjectsWithFetchSpecification(aFetchSpec,
anEditingContext)
finally:
self.unlock()
count+=int(c) # SQLite
return count
def objectsWithFetchSpecification(self, aFetchSpec, anEditingContext):
"""
Fetches and returns a sequence of objects corresponding to the supplied
fetch specification 'aFetchSpec'. These objects are also recorded within
'anEditingContext' 's uniquing table.
If an object is already in the EditingContext, that object is returned,
**not** the one that was fetched --unless the FetchSpecification
'aFetchSpec' is refreshing objects (**not** tested yet).
It does NOT discard any objects that might be deleted in the
'anEditingContext', neither does it returns the objects that were inserted
in anEditingContext but not saved yet ; it is the responsability of
EditingContext.objectsWithFetchSpecification() to do this.
Parameters:
aFetchSpecification -- a FetchSpecification object describing the
objects to be fetched
anEditingContext -- the EditingContext in which the fetched objects
should be registered.
Returns: the fetched objects, all of which are registered within
anEditingContext.
See also: objectsCountWithFetchSpecification(),
EditingContext.objectsWithFetchSpecification()
"""
entitiesNames=[aFetchSpec.entityName()]
if aFetchSpec.isDeep():
import ModelSet
defaultModelSet=ModelSet.defaultModelSet()
entity=defaultModelSet.entityNamed(aFetchSpec.entityName())
entitiesNames+=entity.allSubEntitiesNames()
objects=[]
original_entity_name=aFetchSpec.entityName()
try:
for entityName in entitiesNames:
aFetchSpec.setEntityName(entityName)
self.lock()
try:
channel=self.availableChannel()
channel.selectObjectsWithFetchSpecification(aFetchSpec,
anEditingContext)
finally:
self.unlock()
object=channel.fetchObject()
while object:
objects.append(object)
object=channel.fetchObject()
finally:
# Fixes bug #753147: aFetchSpec shouldn't be changed
aFetchSpec.setEntityName(original_entity_name)
return objects
def refaultObject(self, aDatabaseObject, aGlobalID, anEditingContext):
"""
"""
self.__unimplemented__()
def rootObjectStore(self):
"""
Raises RuntimeError unconditionally: a DatabaseContext is not (yet)
designed to be the 'rootObjectStore()' of an 'ObjectStore' hierarchy. The
only possible root for an hierarchy of ObjectStores is an
'ObjectStoreCoordinator'.
See also: saveChangesInEditingContext()
"""
raise RuntimeError, 'DatabaseContext cannot respond to this message'
def saveChangesInEditingContext(self, anEditingContext):
"""
Unimplemented: a DatabaseContext is not (yet) designed to be the
'rootObjectStore()' of an 'ObjectStore' hierarchy. The only possible root
for an hierarchy of ObjectStores is an 'ObjectStoreCoordinator'.
See also: rootObjectStore()
"""
raise 'Unimplemented', 'DatabaseContext is not designed to be in direct '\
'interaction with an EditingContext. Please use an '\
"'ObjectStoreCoordinator' as the rootObjectStore() instead."
# self.__snapshots=DatabaseContext.SnapshotsTable()
# ...
#
def __del__(self):
self.dispose()
def __unimplemented__(self):
"Raises Unimplemented..."
raise 'Unimplemented', 'Not supported yet'
class SnapshotsTable(SnapshotsHandling.SnapshotsTable):
"""
Internally used to handle local snapshots.
"""
def isSnapshotRefCountingEnabled(self):
"""
Return false: we do not care about snapshot reference counting here,
since the snapshots are only used when saving changes and are
discarded after the transaction finishes.
Overrides
'SnapshotsHandling.SnapshotsTable.isSnapshotRefCountingEnabled()'
"""
return 0
def disableSnapshotRefCounting(self):
"""
Overrides 'SnapshotsHandling.SnapshotsTable.disableSnapshotRefCounting()'
"""
pass
def __abstract__():
raise 'AbstractMethod', 'Left intentionally unimplemented in this class, '\
'subclass should override this method'
##
## Module's variables
##
__defaultDelegate=None
__contextClassToRegister=DatabaseContext
|