Tuesday, September 21, 2010

Php class for simulating Auto Increment in MongoDB




/**
* Static class for generating and
* maintaining records of per-collection
* auto-increment ids
*
* @author Dmitri Snytkine
*
*/
class MongoIncrementor
{
/**
* Name of collection where to
* store the auto-increment values
* You can change it but only before you
* store your first value.
* Once you begin storing values of you
* auto-increments, it's best not to change this, ever!
*
* @var string name of collection
*/
const COLLECTION_NAME = 'Autoincrement';

/**
* The pseudo auto increment handling is done
* by storing collectionName => id
* in Autoincrements collection
*
* We get value, increment it and resave it
* but watch for Errors/Exceptions in order
* to prevent race condition
*
* @param obj $db must pass object of type MongoDB
*
* @param string $collName which collection this id
* is for. This has nothing to do with the name of the collection
* where these generated sequence numbers are stored.
* For example if you need the next id for collection 'STUDENTS',
* then you pass the 'STUDENTS' as $collName value
* This way different values of 'next id' are maintained
* per collection name
*
* @param int initialId if there is no record
* for the collection yet, then start the increment counter
* with this value.
*
* @param int $try this is used for recursive calling this method
* You should NEVER pass this value yourself
*
* @return int value of next id for the collection
*/
public static function nextValue(MongoDB $db, $collName, $minId = 0, $try = 1)
{

if( $try > 100 ){
throw new RuntimeException('Unable to get nextID for collection '.$collName.' after 100 tries');
}

$prevRecordID = null;
$coll = $db->selectCollection(self::COLLECTION_NAME);
$coll->ensureIndex(array('coll' => 1, 'id' => 1), array('unique' => true));

/**
* We use find() instead of findOne() for a reason!
* It's just more reliable this way
*/
$cursor = $coll->find(array('coll' => $collName))->sort(array('id' => -1))->limit(1);
if($cursor && $cursor->hasNext()){
$a = $cursor->getNext();
$prevRecordID = $a['_id'];
} else {
$a = array('coll' => $collName, 'id' => $minId);
}

$prevID = $a['id'];
$newId = ($a['id'] + 1);

/**
* Remove the _id from record, otherwise
* we will be unable to insert
* a new record if it already has the same _id
* This way a new _id will be auto-generated for us
*/
unset($a['_id']);
$a['id'] = $newId;


/**
* Wrapping this inside try/catch so that if
* another process inserts the same value of coll/id
* between the time we selected and updated this
* it will throw exception or return false and then
* we will try again up to 100 times
*
* In Case of duplicate key Mongo throws Exception,
* but just in case it will change in the future,
* we also test if $ret is false
*/
try{
/**
* Using fsync=>true because its very critically important
* to actually write the row to disc, otherwise if database
* goes down we will lose the correct value
* of our increment ID
*/
$ret = $coll->insert($a, array('fsync' => true));
if(!$ret){
$try++;

return self::nextValue($db, $collName, $initialId, $try);
}

/**
* Insert successfull
* now delete previous record(s)
*/
if(null !== $prevRecordID){
$removed = $coll->remove(array('_id' => $prevRecordID)); //, array('fsync' => true) // not very important to fsync
}

} catch (MongoException $e){

$try++;

return self::nextValue($db, $collName, $initialId, $try);
}

return $newId;
}
}

No comments:

Post a Comment