简体   繁体   中英

Instantiating objects within result sets… best practices?

I'm developing an intranet site for reporting on data. I've developed various classes for Customer, Order, Item, Invoice, etc that all relate to each other in one way or another. Each class constructor queries a MySQL database (multiple queries) and populates the properties accordingly.

Currently, to try and keep code convenient and consistent, I'm relying a lot on instantiating classes in my reporting. The problem is, each class constructor may have a bunch of MySQL queries within them, pulling or calculating relevant properties from various sources. This causes a performance hit because of all the queries within loops. It's usable, and I wont ever have a ton of users at once.. but it could be much faster.

For example, let's say I'm listing the last 50 orders from a customer. The way I'm doing it now, I'll typically write a simple query that returns the 50 order ID's alone for that customer. Then while looping through the results, I'll instantiate a new order for each results. Sometimes I may even go one level deeper and then instantiate a new object for each item within the order.

Some pseudocode to give the idea....

$result = mysql_query("SELECT DISTINCT id FROM orders WHERE customer = 1234 LIMIT 50");
while ($row = mysql_fetch_array($result, MYSQL_NUM)) {
    $order = new Order($row['id']); // more MySQL queries happen in constructor
    // All of my properties are pre-calculated and formatted
    // properly within the class, preventing me from having to redo it manually
    // any time I perform queries on orders
    echo $order->number;
    echo $order->date;
    echo $order->total;
    foreach($order->items as $oitem) {
        $item = new Item($oitem); // even more MySQL queries happen here 
        echo $item->number;
        echo $item->description;
    }
}

I may only use a few of the object properties in a summary row, but when I drill down and view an order in more detail, the Order class has all the properties I need ready to go, nice and tidy.

What's the best way this is typically handled? For my summary queries, should I try to avoid instantiating classes and get everything in one query that's separate from the class? I'm worried that will cause me to have to manually do a lot of background work I typically do within the class every single time I do a result set query. I'd rather make a change in one spot and have it reflect on all the pages that I'm querying the effected classes/properties.

What are other ways to handle this appropriately?

This question is pretty open-ended, so I'm going to give a fairly broad answer and try to not get too carried away. I'll start out by saying that an ORM solution like Doctrine's, Propel's, or Symfony's is probably the most ideal for managing relational objects, but not always practical to implement quickly or cleanly (it can take a while to learn the ORM and then convert existing code). Here's my take on a more light-weight approach.

To start out, it might help to take your database queries out of the class constructors so that you can have better control over when you access your database. One strategy is to add static methods to your class(es) for fetching results. In addition, you can provide options to "prefetch" child objects so that you can perform your queries in bulk. So to go into your example, the external API would look something like this:

$orders = Order::getOrders(array(
    'items' => true
));

The idea here is that you want to get an array of orders with the getOrders() method and tell getOrders() to fetch item child objects at the same time. From outside the Order class, it's pretty simple: just pass in an array with the 'items' key set to true . Then within the Order class:

class Order
{

    public $items = null;

    public static function getOrders(array $options = array())
    {
        $orders = array();

        $result = mysql_query("SELECT DISTINCT id FROM orders WHERE customer = 1234 LIMIT 50");
        while ($row = mysql_fetch_array($result, MYSQL_NUM)) {
            $order = new Order($row); 
            $orders[$order->id] = $order;
        }

        // if we're asked to fetch items, fetch them in bulk
        if (isset($options['items']) && $options['items'] === true) {
            $this->items = array();
            $items = Item::getItemsForOrders($orders);
            foreach ($items as $item) {
                $orders[$item->orderId]->items[] = $items;
            }
        }

        return $orders
    }

    public function __construct(array $data = array())
    {
        // set up your object using the provided data
        // rather than fetching from the database
        // ...
    }

    public function getItems()
    {
        if ($this->items === null) {
            $this->items = Item::getItemsForOrders(array($this))
        }

        return $items;
    }
}

And in your Item class:

class Item
{
    public $orderId = null;

    public static function getItemsForOrders(array $orders, array $options = array())
    {
        // perform query for ALL orders at once using 
        // an IN statement, returning an array of Item objects
        // ...
    }
}

Now, if you know that you will need items when you're getting your orders, pass in a true for the 'items' option:

$orders = Order::getOrders(array(
    'items' => true
));

Or if you don't need items, don't specify anything:

$orders = Order::getOrders();

Either way, when you are looping through your orders, the API is identical for accessing items:

// the following will perform only 2 database queries
$orders = Order::getOrders(array(
    'items' => true
));
foreach ($orders as $order) {
    $items = $order->getItems(); 
}

// the following will perform 1 query for orders 
// plus 1 query for every order
$orders = Order::getOrders();
foreach ($orders as $order) {
    $items = $order->getItems(); 
}

As you can see, providing the 'items' option can lead to more efficient use of the database, but if you just need orders without messing around items , you can do that too.

And because we provide an array of options to getOrders() , we can easily extend our functionality to include flags for additional child objects (or anything else that should be 'optional'):

$orders = Order::getOrders(array(
    'items' => true, 
    'tags' => true, 
    'widgets' => true, 
    'limit' => 50, 
    'page' => 1
));

...and you can proxy those options to child objects if needed:

// ...
// in the `Order::getOrders()` method, when getting items...
$items = Item::getItemsForOrders($orders, array(
    'tags' => (isset($options['tags']) && $options['tags'] === true)
));

If you aren't judicious about what should or should not be optional when fetching objects, this approach can become bloated and daunting to maintain, but if you keep your API simple and only optimize when you need to, it can work really well. Hope this helps.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM