How to manage multiple db gateways for one action

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

How to manage multiple db gateways for one action

Post 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?
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post 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...
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post 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.
(#10850)
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post 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.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post 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.
(#10850)
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post 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.
wei
Forum Contributor
Posts: 140
Joined: Wed Jul 12, 2006 12:18 am

Post 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.
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post 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.
Post Reply