Page 1 of 1

How to manage multiple db gateways for one action

Posted: Fri Sep 07, 2007 2:39 pm
by Begby
I know my title doesn't make too much sense...

Anyways, I am using domain objects, validators, and mappers (gateways).

So for a given table I have a domain object that represents an individual record, 0 or more validators for validating a record, and then a mapper which is what reads and writes to a database, so it might look something like this

Code: Select all

$mapper = new FCP_Item_Mapper($this->db()) ;
$validator = new FCP_Item_Validator($mapper) ;
$item = new FCP_Item() ;

$item->setSKU('somesku') ;
$item->setName('product name') ;

if ($validator->validate($item))
{
  $mapper->insert($item) ;
}
That should be pretty straight forward.

The problem I am having is figuring out what to do when multiple tables need to be updated for a single action. For instance, canceling an order involves the following:

-Mark the order as cancelled in the order table
-Add a orderHistory record saying the order was cancelled
-Mark any shipment records as cancelled
for each of the items in the order do this
-Add the item quantity back into inventory
-Check for items on backorder and release orders if possible
-Add a history record for the item

Here are possible solutions

1. In the controller instantiate an Order_Mapper, an Order_History_Mapper, an Order_Shipment_Mapper, an Item_Inventory_Mapper, an Order_Item_Mapper, and an Order_History_Mapper and then run the required methods. - But then I would need to redo all of that everywhere an order is cancelled, so that is not a good option.

2. Have the OrderMapper do all of this. - This would increase dependency on a lot of other classes, the mapper should only be concerned about individual order records.

3. Have another set of classes that do this. - These classes would either need to be passed a registry of all the mappers or instantiate them internally.

4. Have another specialized mapper that works with more than one table. In this case it wouldn't use mappers but instead carry out the action by directly using SQL. - If I change the table structure I would then need to go back and change this class along with the individual mappers.

5. Do #4 then try to put as much as possible into stored procedures

6. Remove all the individual mappers and have the order mapper just take care of everything.


Anyone have any other ideas on how to approach this?

Posted: Fri Sep 07, 2007 2:56 pm
by s.dot
Yeah, it seems you're missing (unless i missed because I breifed), a simple call to a query.

Code: Select all

$mapper->update('table1');
$mapper->update('table2');
Sure this creates an unneeded query, but optimisation isn't called for yet.


#4 Smells like bad code design. I don't think changing a table structure should ever require the need to change a class code.

Posted: Fri Sep 07, 2007 3:23 pm
by Begby
scottayy wrote:Yeah, it seems you're missing (unless i missed because I breifed), a simple call to a query.
I have no idea what you mean by that


I just perused my Patterns of Enterprise Application Architecture book and found a pattern called 'Service Layer'. It seems to be just what I want but I think that unit testing it might be nigh impossible unless I create a million mock objects. I could use real mappers in the service layer unit tests though too. Hrmm...

Posted: Fri Sep 07, 2007 3:52 pm
by Christopher
It does not look like you have much choice other than to either pass multiple connection to the mapper (and let it sort it out), or have multiple mappers. I think I would try he latter first.

Posted: Fri Sep 07, 2007 4:06 pm
by Begby
I think I will do something like this with the service layer pattern.

(the real one I think is going to be a lot more complex than this)

Code: Select all

class FCP_Order_Service extends FCP_Application_Service_Abstract
{

  public function placeOrder(FCP_Order $order)
  {
     $invService = new FCP_Item_Inventory_Service($this->db) ;
     
     foreach ($order->getOrderItems as $item)
     {
        $invService->removeInventory($item->getItemID(), $item->getQty(), TRANS_TYPE_ORDER_PLACED) ;
     }

     $histMapper = new FCP_Order_History_Mapper($this->db) ;
     $hist = new FCP_Order_History() ;
     $mapper = new FCP_Order_Mapper($this->db) ;
     
     $hist->setType(TRANS_TYPE_ORDER_PLACD) ;
     $hist->setDate(new FCP_DateTime(time)) ;

     $mapper->insert($order) ;
     $histMapper->insert($history) ;
  }

}
Then from the controller

Code: Select all

$order = new FCP_Order() ;
// populate the order and validate it

$service = new FCP_Order_Service($this->db) ;
$service->placeOrder($order) ;
So then within my inventory service I would reference my inventory mapper and item history mappers to deduct the inventory and add in the history records.

I think each basic use case will be encapsulated as a method within a service, then the service will carry out that case using a variety of mappers and domain objects.

Posted: Fri Sep 07, 2007 4:28 pm
by Christopher
Adding a layer can solve problems like this, but how to you then handle errors. There are potentially many points of failure with this type of code. I might try inversion instead, but either may work better -- you will just have to try it.

Posted: Fri Sep 07, 2007 7:24 pm
by Begby
The problem here with inversion is that I would need to create like 6 or more mappers for a specific call. (if I am understanding that by inversion you are suggesting using dependency injection). So that means to call say createOrder() using the order mapper I would need to pass it some mappers, some of which need other mappers (like the inventory mapper would need a reference to the history mapper and also the backorder mapper). That might look like this

Code: Select all

$invMapper = new FCP_Item_InventoryMapper($this->db, new FCP_BackOrder_Mapper($this->db), new FCP_Item_History_Mapper($this->db)) ;
$orderMapper = new FCP_Order_Mapper($this->db, $invMapper, new FCP_Order_History_Mapper($this->db));
And I would probably need to add to that.


Yes, there are a lot of points of failure as this means a whole bunch of class methods need to get called. I have unit tests for everything right now though, and also I do a lot of sanity checks and throw exceptions with an exception class for every package/subpackage. Any exceptions and errors will get logged to a database.

One thing though, I don't really know how to go about unit testing a layer like this. I could test all the dependicies separately, then run heavier tests on the service layer and do manual queries to make sure that the state of the database is correct after each method.

Posted: Sat Sep 08, 2007 1:42 am
by wei
A service layer is a good choice for this case. Since you probably need to perform a transaction on those changes. Moreover, transactions doesn't usually belong in the mappers since transactions are connection oriented. For error handling, thow a specific exception or let the transaction exceptions follow through. Later, this service layer can also be used to create an API, e.g. soap, etc.

Posted: Sun Sep 09, 2007 9:09 am
by Begby
wei wrote:A service layer is a good choice for this case. Since you probably need to perform a transaction on those changes. Moreover, transactions doesn't usually belong in the mappers since transactions are connection oriented. For error handling, thow a specific exception or let the transaction exceptions follow through. Later, this service layer can also be used to create an API, e.g. soap, etc.
Thank you, I think I am going to go this way. I will work out a way to allow a mapper function to be in a transaction and then do a rollback on an exception. I think that will be keen.

The requirements specify multiple methods to create orders, via a REST and SOAP API, and also through an import adapter, so I think this will work well for that.