New rdbms.finder API

at 2007-01-24 in RFCs by friebe (0 comments)

With RFC #0099 implemented, the XP framework's O/R-mapping api (rdbms.Peer, rdbms.DataSet, rdbms.Criteria, ...) has become even more flexible. I'd like to showcase some improvements I made to existing sourcecode yesterday.

Overview
Basically, a Finder subclass is a collection of named queries (that is, methods that return SQLExpression instances). Before finders existed, one was basically restricted to using the DataSets static getBy... methods (those are generated for the primary key and each index on the table) or by manually getting the corresponding Peer object and utilizing its doSelect() or iteratorFor() facilities, passing them Criteria or Statements. By codifying these in a finder class, we benefit from reuse.

But there's more!

Example
Here's an abbreviated version of a DataSet subclass representing customer entities:

  class Customer extends DataSet {

public
static function getByCustomer_id($customer_id) {
// ...
}
}

To get a customer by the customer id, we currently use (and still can):
  $customer= Customer::getByCustomer_id($customer_id);

Simple enough:) There are downsides of this approach, though:
  • It's not very flexible
    Imagine we had a flag "status" in our customer table which would indicate whether customers are currently active or not. Now if we only want active customers, we would either have to write a wrapper class such as:
      class CustomerHelper extends Object {
    public
    static function getActiveCustomer($customer) {
    if
    (!($customer= Customer::getByCustomer_id($customer_id))) return NULL; // No customer found
    if (1 != $customer->getStatus()) return NULL; // Found but not active
    return $customer;
    }
    }

    ...or use criteria:
      $customer= current(Customer::getPeer()->doSelect(Criteria::newInstance()
    ->add
    ('customer_id', $customer_id, EQUAL)
    ->add
    ('status', 1, EQUAL)
    ));

  • Existance checks must be performed manually
    The getBy...-methods return NULL if nothing is found. In some situations, this can lead to quite a bit of sanity-check-blocks. In most of our webservices, we have something like:
      class DomainHandler extends Object {

    #[@webmethod]
    function getByContractId($contract_id) {
    if
    (!($contract= Contract::getByContract_id($contract_id)) {
    throw
    new ElementNotFoundException('Contract #'.$contract_id.' does not exist!');
    }
    return Domain::getByContract_id
    ($contract_id);
    }
    }

    This is necessary for web service users to be able to distinguish whether they got an empty result because the contract does not exist or because the contract simply does not have any domains.


Using finders
Here's the most basic form of a customer finder:
  class CustomerFinder extends Finder {

public
function getPeer() {
return Customer::getPeer
();
}

#[@finder(kind= ENTITY)]
public function byCustomerId($id) {
return
new Criteria(array('customer_id', $id, EQUAL));
}
}

...and how to use it:
  $cf= new CustomerFinder();
$customer= $cf->find($cf->byCustomerId($customer_id));

Admittedly, at the first glance, this isn't any shorter than using the getByCustomer_id() method. We'll highlight the benefits as we continue.

Replacing the Helper classes
By adding another finder method such as:
  #[@finder(kind= ENTITY)]
public function byActiveCustomerId($id) {
return Criteria::newInstance
()
->add
('customer_id', $customer_id, EQUAL)
->add
('status', 1, EQUAL)
);
}

...we really aren't changing much, we're just renaming the helpers to finders. But it's simpler: See how the getPeer()/doSelect() part is missing? We'll let the users of our finder class decide how they want their results:
  // Customer entity when found, NULL otherwise
$customer= $cf->find($cf->byActiveCustomerId($cid));

// Array of Customer entities, empty when nothing is found.
$customer= $cf->findAll($cf->byActiveCustomerId($cid));

// Customer entity when found, NoSuchElementException thrown by next() otherwise
$customer= $cf->iterate($cf->byActiveCustomerId($cid))->next();


Existance checks simplified
If all you want is to throw exceptions when you encounter unfound entities, you can utilize the get() methods instead of their find equivalents. This:
  $customer= Customer::getByCustomer_id($cid);
if
(!$customer) throw new ElementNotFoundException('Customer #'.$cid.' not found');
$contracts= Contract::getByCustomer_id($customer->getCustomer_id());

...can now be replaced by:
  $cf= new CustomerFinder();
$contracts= Contract::getByCustomer_id($cf->get($cf->byCustomerId($cid))->getCustomer_id());

The get() method will throw a NoSuchEntityException when no customer exists by the given customer id. No NULL checks need to be performed, no possible fatal error situations occur. If you forget a NULL-check in the "old" code above, you'll see a nice "Call to a member-function of a non-object" error:)

Constants
Let's say the "status" field in the customer table above contains a bitfield. There are flags such as active, locked, ..., or'ed together.

The finder method from above could be rewritten as follows:
  #[@finder(kind= ENTITY)]
public function byCustomerIdAndStatus($id, $status) {
return Criteria::newInstance
()
->add
('customer_id', $customer_id, EQUAL)
->add
('status', $status, BIT_AND)
);
}

#[@finder(kind= ENTITY)]
public function byActiveCustomerId($id) {
return
$this->byCustomerIdAndStatus($id, 1);
}

Because the hardcoded "1" is not very nice, and people will have to look up what it means, we can simply add class constants to the finder class:
  class CustomerFinder extends Finder {
const ACTIVE = 0x0001;
const LOCKED = 0x0002;

// ...
}

and change the byActiveCustomerId() method to contain:
  return $this->byCustomerIdAndStatus($id, self::ACTIVE);

Because class constants are public, anyone can use them and produce more readable code wherever they do so.

Refining results
Say we have a very rare case of having to find an active customer that is also a premium customer (usually, the premium flag would only be read off the results and used to activate links or so). Instead of creating a seldomly-used finder method, we can leave this task to the implementer:
  $cf= new CustomerFinder();
$premium= $cf->get($cf->byActiveCustomerId($cid)->add('is_premium', TRUE, EQUAL));


Reflective use
The finder classes provide built-in methods for generic access to the methods they provide. The method() method will return a finder by the given name:
  $customer= $cf->find($cf->method('byActiveCustomerId')->invoke(array($cid)));

This is a something you may want to use in a GUI.



Subscribe

You can subscribe to the XP framework's news by using RSS syndication.


Categories

News
General
PHP5
Announcements
RFCs
Further reading
Examples
Editorial
EASC
Experiments
Unittests
Databases
5.8-SERIES
Unicode
Language
5.9-SERIES

Related

Find related articles by a search for «New».