ColdBox + MongoDB

Hi again folks!

I’m tinkering with a new CB project using mangoDB and am wondering if anyone can share their thoughts and approaches to building an api-like app with Mongo as the datasource. I’ve gotten really used to using CB’s populateModel for persisting data, but as far as I can tell this wouldn’t work necessarily with Mango. What i’m trying to wrap my head around is designing a service layer that can serialize and deserialize objects for the GET and POST/PUT of data, but also having a dynamic way to take the schema-less data that comes into a handler and populate that into a model. The catch being that the model / object is built dynamically based on what is in the request. Any thoughts on how I would approach this?

Cheers,

Nolan

Hi there Nolan,

This might get you started in the right direction. While at
cfObjective (ANZ) last year I played around with CF/MongoDB and you're
right the serialise/deserialise bits are where you miss ORM.

Anyway - this has never been in production so you might need to
benchmark it and I got some/most of the code from one of the populate
projects somewhere (ha! well I was coding it up during
presentations :D)
It's pretty raw, but follows (roughly) the BaseOrmService,
VirtualEntityService used in Coldbox ORM

Happy for you to use this however you like. Not sure if it'll support
nested objects/models.

Cheers
Steve

BaseModelObject ------------------------------
component output="false"
{
  public Struct function asStruct()
  {
    return deserializeJSON(serializeJSON(this));
  }

  public any function populate( required any data, any propList =
arrayNew(1))
  {
    var metadata = getMetadata(this);
    param name="metadata.cleanseInput" default="false";

    for(var i=1; i<=arrayLen(metadata.properties); i++)
    {
      if(not arraylen(arguments.propList) OR
arrayContains(arguments.propList,metadata.properties[i].name))
      {
        if(NOT StructKeyExists(metadata.properties[i],"fieldType") OR
metadata.properties[i].fieldType EQ "column")
        {
          if(StructKeyExists(arguments.data,metadata.properties[i].name))
          {
            /*The property has a matching argument*/
            local.varValue = arguments.data[metadata.properties[i].name];
            //For nullable fields that are blank, set them to null
            if((NOT StructKeyExists(metadata.properties[i],"notNull") OR NOT
metadata.properties[i].notNull) AND NOT Len(local.varValue))
            {
              _setPropertyNull(metadata.properties[i].name);
            }
            else
            {
              //Cleanse input?
              param name="metadata.properties[i].cleanseInput"
default="#metadata.cleanseInput#";
              if(metadata.properties[i].cleanseInput)
              {
                local.varValue = _cleanse(local.varValue);
              }
              _setProperty(metadata.properties[i].name,local.varValue);
            }
          }
        }
        else if(metadata.properties[i].type EQ "array")
        {
          if(isDefined("arguments.data.#metadata.properties[i].name#"))
          {
            var entityName = metadata.properties[i].cfc;
            var items =
evaluate("arguments.data.#metadata.properties[i].name#");
            writeDump(var=items,label="in populate");
            var entityArray = arrayNew(1);
            for(var j=1; j<=arrayLen(items); j++)
            {
              var entity = createObject("component",entityName);
              entity.populate(items[j]);
              arrayAppend(entityArray,entity);
            }
            _setProperty(metadata.properties[i].name,entityArray);
          }
        }
        //do many-to-one
        else if(metadata.properties[i].fieldType EQ "many-to-one")
        {
  
if(StructKeyExists(arguments.data,metadata.properties[i].fkcolumn))
          {
            local.fkValue = arguments.data[metadata.properties[i].fkcolumn];
          }
          else
if(StructKeyExists(arguments.data,metadata.properties[i].name))
          {
            local.fkValue = arguments.data[metadata.properties[i].name];
          }
          if(StructKeyExists(local,"fkValue"))
          {
            local.varValue =
EntityLoadByPK(metadata.properties[i].name,local.fkValue);
            if(IsNull(local.varValue))
            {
              if(NOT StructKeyExists(metadata.properties[i],"notNull") OR NOT
metadata.properties[i].notNull)
              {
                _setPropertyNull(metadata.properties[i].name);
              }
              else
              {
                throw(detail="Trying to load a null into the
#metadata.properties[i].name#, but it doesn't accept nulls.");
              }
            }
            else
            {
              _setProperty(metadata.properties[i].name,local.varValue);
            }
          }
        }
      }
    }
  }

  function _cleanse (required any data)
  {
    return HTMLEditFormat(arguments.data);
  }

  private void function _setProperty (required any name, any value)
  {
    var theMethod = this["set" & arguments.name];
    if(IsNull(arguments.value))
    {
      theMethod(javacast('NULL', ''));
    }
    else
    {
      theMethod(arguments.value);
    }
  }
  private void function _setPropertyNull(required any name)
  {
    _setProperty(arguments.name);
  }

}
end BaseModelObject---------------------------

User -----------------------------------------
component output="false" accessors="true" extends="BaseModelObject"
{
  property name="id" type="any";
  property name="firstname" type="String";
  property name="lastname" type="string";
  property name="email" type="string";
  property name="username" type="string";
  property name="password" type="string";
  property name="testStruct" type="Struct";

  property name="siteID" type="numeric";

  property name="memberships" singularname="membership" type="array"
cfc="BaseMembership";

  public User function init()
  {
    setSiteId(application.siteID);
    return this;
  }

}
end User -------------------------------------

BaseService ----------------------------------
component output="false" accessors="true"
{
  property name="entityName" type="String" required="true" ;
  property name="collectionName" type="String" required="true" ;

  BaseService function init()
  {
    javaloaderFactory =
createObject('component','cfmongodb.core.JavaloaderFactory').init();
    mongoConfig =
createObject('component','cfmongodb.core.MongoConfig').init(dbName="mongorocks",
mongoFactory=javaloaderFactory);
    mongo =
createObject('component','cfmongodb.core.Mongo').init(mongoConfig);
    if(isDefined("arguments.clearCollection") &&
isDefined("arguments.collectionName"))
    {
      if(arguments.clearCollection)
      {
        var collection = mongo.getDBCollection(arguments.collectionName);
        collection.remove({});
      }
    }
    if(isDefined("arguments.entityName") && len(arguments.entityName))
    {
      setEntityName(arguments.entityName);
    }
    return this;
  }

  private Struct function asStruct(any entity)
  {
    return deserializeJSON(serializeJSON(arguments.entity));
  }

  any function new(string entityName,struct properties=structnew())
  {
    var entity = createObject("component", arguments.entityName);
    var key = "";
    var excludes = "entityName,properties";

    // Properties exists?
    if( NOT structIsEmpty(arguments.properties) ){
      entity.populate(arguments.properties );
    }
    //else{
    //
populate(target=entity,memento=arguments,exclude="entityName,properties");
    //}

    return entity;
  }

  any function get(required string entityName,required any id)
  {
    collection = mongo.getDBCollection( getCollectionName() );

    // Check if ID=0 or empty to do convenience new entity
    if( isSimpleValue(arguments.id) and ( arguments.id eq 0 OR
len(arguments.id) eq 0 ) ){
      return new(arguments.entityName);
    }
    // check if id exists so entityLoad does not throw error
    if( (isSimpleValue(arguments.id) and len(arguments.id)) OR NOT
isSimpleValue(arguments.id) ){
      var entityStruct = collection.findById(arguments.id);
      writeDump(var=entityStruct, label="entity struct in base service");
      // Check if not null, then return it
      if( NOT isNull(entityStruct) ){
        var entity = createObject("component",arguments.entityName);
        entity.populate(entityStruct);
        entity.setId(entityStruct._id.toString());
        return entity;
      }
    }
  }

  any function save(any entity)
  {
    var entityAsStruct = asStruct(arguments.entity);
    collection = mongo.getDBCollection( getCollectionName() );
    collection.save(entityAsStruct);
    entity.populate(entityAsStruct);
    entity.setId(entityAsStruct._id.toString());
    return entity;
  }

}
end BaseService-------------------------------

VirtualObjectService -------------------------
component extends="BaseService" output="false" accessors="true"
{
  property name="entityName" type="String" required="true" ;
  property name="collectionName" type="String" required="true" ;

  VirtualObjectService function init(required String entityName,
required String collectionName, boolean clearCollection=false)
  {
    super.init(argumentCollection=arguments);

    // Set the local entity to be used in this virtual entity service
    setEntityName(arguments.entityName);
    setCollectionName(arguments.collectionName);

    return this;
  }

  any function new(){
    arguments.entityName = this.getEntityName();
    return super.new(argumentCollection=arguments);
  }
  any function get(required any id){
    arguments.entityName = this.getEntityName();
    return super.get(argumentCollection=arguments);
  }

}
end VirtualObjectService ---------------------

UserService ----------------------------------
component extends="VirtualObjectService" output="false"
{
  public UserService function init(String collectionName="users",
boolean clearCollection=false)
  {
  
super.init(entityName="User",collectionName=arguments.collectionName,
clearCollection=arguments.clearCollection);
    return this;
  }

}
end UserService ------------------------------

Hi Stephen,

Thanks for the code samples. This is very similar to some of my initial thoughts so you've saved me a tonne of time by providing a rough framework. I recognize that populate method! Bob Silverberg and I live in the same city (Toronto). Do you happen to have an example of how you would run this within a coldbox handler? In transit right now. Will dive into this further when I get back on my laptop. Thanks again!

Nolan Dubeau

Load *.*,8,1

No sorry - this didn't make it very far.
I did write a couple of (very rudimentary) tests to prove to myself
that it would work and that was it.

However I imagine you could fold this into coldbox fairly easily. Let
me know how you go.

Ahh yes Bob Silverberg is where I got the code from - from memory I
had to script-ify it.

Some of the test code dumped below - it's fairly self-explanatory but
I've added some extra annotation for you just in case

  function beforeTests()
  {
    userService = new UserService(collectionName="users_test",
clearCollection = true); <- create a new empty mongo collection
    testData =
{firstname="TestUser",lastname="TestLastname",email="test@email.com",username="test_user",password="password"};
<- test user struct to populate a user obj. simulates a form post
  }

function testUserServiceGetUserWithZeroID()
  {
    user = userService.get(id=0); <- sometimes passing in 0 as the ID to
get a new obj can be good
    assertIsTypeOf(user, "User", "Service returned incorrect entity");
  }
  function testUserServiceGetUser()
  {
    //fake up what would be a variable passed in
    user = userService.new(properties=testData); <- creates a new user
and populates it from the "form" struct
    user = userService.save(user);
    assertIsTypeOf(user, "User", "Service returned incorrect entity");

    userX = userService.get(id=user.getId()); <- gets the user we just
save
    assertIsTypeOf(userX, "User", "Service returned incorrect entity");
<- tests that the service is converting the mongo "struct" to a User
object
    assertEquals(userX.getId(), user.getId(), "Service returned
incorrect entity");
  }

  function testUserServiceCreateUser()
  {
    user = userService.new();
    assertIsTypeOf(user, "User", "Service returns invalid object");

  }
  function testUserServiceCreateUserWithProperties()
  {
    user = userService.new(properties=testData);
    assertIsTypeOf(user, "User", "Service returns invalid object");
    assertEquals("TestUser", user.getFirstname(), "Service returns
invalid property");
  }

  function testUserServiceSaveUser()
  {
    user = userService.new(properties=testData);
    assertTrue(isNull(user.getId()), "User ID invalid");
    user = userService.save(user);
    assertFalse(isNull(user.getId()), "User ID invalid");
  }

Hi Stephen,

thanks for sending the tests over. I’ll give it a shot and let you know how I make out.

Cheers,

Nolan

Hey Nolan - how’d you go with this?