Skip to content

Commit

Permalink
Add preload models ability for findAll reduces sql request #1227
Browse files Browse the repository at this point in the history
  • Loading branch information
nadar committed Apr 13, 2017
1 parent b8871c3 commit 7651d01
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The changelog contains informations about bug fixes, new features or bc breaking

### Added

- [#1227](https://github.com/luyadev/luya/issues/1227) Added preloadModels() method for the Menu Query in order to collect all models for the given request. This can strongly reduce the sql count when working with properties or models.
- [#1266](https://github.com/luyadev/luya/issues/1266) render() method for the mailer component in order to provide controller template files.

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/app-cmsproperties.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ A very common scenario is to add properties to an existing menu item like an ima

This method allows you find and evaluate properties for menu items and allows you also to use `Yii::$app->menu->current->getProperty('xyz')`.

> When dealing with large menus you can preload the models (with its properties) for a given menu query use {{luya\cms\menu\Query::preloadModels}} or {{luya\cms\Menu::findAll}} with second statement `true`.
#### in Layouts

In the layout you can always get the current propertys based on the current active menu item.
Expand Down
7 changes: 4 additions & 3 deletions modules/cms/src/Menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -447,13 +447,14 @@ public function find()
* Wrapper method to get all menu items for the current language without hidden items for
* the specific where statement.
*
* @param array $where See \luya\cms\menu\Query::where()
* @param array $where See {{\luya\cms\menu\Query::where}}
* @param boolean $preloadModels Whether to preload all models for the given menu Query. See {{luya\cms\menu\Query::preloadModels}}
* @see \luya\cms\menu\Query::where()
* @return \luya\cms\menu\QueryIterator
*/
public function findAll(array $where)
public function findAll(array $where, $preloadModels = false)
{
return (new MenuQuery())->where($where)->all();
return (new MenuQuery())->where($where)->preloadModels($preloadModels)->all();
}

/**
Expand Down
10 changes: 10 additions & 0 deletions modules/cms/src/menu/Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,16 @@ public function getModel()
return $this->_model;
}

/**
* Setter method for the Model.
*
* @param null|\luya\cms\models\Nav $model The Nav model Active Record
*/
public function setModel($model)
{
$this->_model = $model;
}

/**
* This method allows you the retrieve a property for an page property. If the property is not found false will be retunrend
* otherwhise the property object itself will be returned (implements `\admin\base\Property`) so you can retrieve the value of the
Expand Down
70 changes: 48 additions & 22 deletions modules/cms/src/menu/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Yii;
use luya\cms\Exception;
use yii\base\Object;

/**
* Menu Query Builder.
Expand Down Expand Up @@ -45,21 +46,14 @@
* @since 1.0.0
* @author Basil Suter <basil@nadar.io>
*/
class Query extends \yii\base\Object
class Query extends Object
{
private $_where = [];

private $_lang = null;

private $_menu = null;

private $_whereOperators = ['<', '<=', '>', '>=', '=', '==', 'in'];

private $_with = ['hidden' => false];

private $_offset = null;
/**
* @var array An array with all available where operators.
*/
protected $whereOperators = ['<', '<=', '>', '>=', '=', '==', 'in'];

private $_limit = null;
private $_menu = null;

/**
* Getter method to return menu component
Expand All @@ -75,6 +69,8 @@ public function getMenu()
return $this->_menu;
}

private $_where = [];

/**
* Query where similar behavior of filtering items.
*
Expand Down Expand Up @@ -133,7 +129,7 @@ public function getMenu()
public function where(array $args)
{
foreach ($args as $key => $value) {
if (in_array($value, $this->_whereOperators, true)) {
if (in_array($value, $this->whereOperators, true)) {
if (count($args) !== 3) {
throw new Exception(sprintf("Wrong where(['%s']) condition, see https://luya.io/api/luya-cms-menu-Query#where()-detail for all available conditions.", implode("', '", $args)));
}
Expand All @@ -160,6 +156,8 @@ public function andWhere(array $args)
return $this->where($args);
}

private $_lang = null;

/**
* Changeing the container in where the data should be collection, by default the composition
* `langShortCode` is the default language code. This represents the current active language,
Expand All @@ -175,22 +173,44 @@ public function lang($langShortCode)
return $this;
}

private $_with = ['hidden' => false];

/**
* @param string|array $types can be a string containg "hidden" or an array with multiple patters
* With/Without expression to hidde or display data from the Menu Query.
*
* @param string|array $types can be a string containg "hidden" or an array with multiple with statements
* for example `['hidden']`. Further with statements upcoming.
* @return \luya\cms\menu\Query
*/
public function with($types)
{
$types = (array) $types;
foreach ($types as $type) {
if (array_key_exists($type, $this->_with)) {
if (isset($this->_with[$type])) {
$this->_with[$type] = true;
}
}

return $this;
}

private $_preloadModels = false;

/**
* Preload Mmodels for the given Menu Query.
*
* When menu a {{luya\cms\menu\Item::getModel}} method is called it will lazy the given {{luya\cms\models\Nav}} Model.
* This can be slow on large menus, therfore you can preload all models for given Menu Query by enabling this method.
*
* @param boolean $preloadModels Whether to preload all {{luya\cms\menu\Item}} models for {{luya\cms\menu\Item::getModel}} or not.
* @return \luya\cms\menu\Query
*/
public function preloadModels($preloadModels = true)
{
$this->_preloadModels = $preloadModels;

return $this;
}

/**
* Return the current language from composition if not set via `lang()`.
Expand All @@ -206,6 +226,8 @@ public function getLang()
return $this->_lang;
}

private $_limit = null;

/**
* Set a limition for the amount of results.
*
Expand All @@ -221,6 +243,8 @@ public function limit($count)
return $this;
}

private $_offset = null;

/**
* Define offset start for the rows, if you defined offset to be 5 and you have 11 rows, the
* first 5 rows will be skiped. This is commonly used to make pagination function in combination
Expand Down Expand Up @@ -264,7 +288,7 @@ public function one()
*/
public function all()
{
return static::createArrayIterator($this->filter($this->menu[$this->getLang()], $this->_where, $this->_with), $this->getLang(), $this->_with);
return static::createArrayIterator($this->filter($this->menu[$this->getLang()], $this->_where, $this->_with), $this->getLang(), $this->_with, $this->_preloadModels);
}

/**
Expand All @@ -283,24 +307,26 @@ public function count()
*
* @param array $data The filtere results where the iterator object should be created with
* @param string $langContext The language short code context, if any.
* @param integer $preloadModels Whether the models should be preload or not.
* @return \luya\cms\menu\QueryIterator
*/
public static function createArrayIterator(array $data, $langContext, $with)
public static function createArrayIterator(array $data, $langContext, $with, $preloadModels = false)
{
return (new QueryIteratorFilter(Yii::createObject(['class' => QueryIterator::className(), 'data' => $data, 'lang' => $langContext, 'with' => $with])));
return (new QueryIteratorFilter(new QueryIterator(['data' => $data, 'lang' => $langContext, 'with' => $with, 'preloadModels' => $preloadModels])));
}

/**
* Static method to create the item object itself, is used for the one() method and in the current() method
* of the QueryIterator class.
*
* @param array $itemArray The item array data for the object
* @param string $langContext The language short code context, if any.
* @param string $langContext The language short code context, if any.
* @param null|\luya\cms\models\Nav The nav model from the preload stage.
* @return \luya\cms\menu\Item
*/
public static function createItemObject(array $itemArray, $langContext)
public static function createItemObject(array $itemArray, $langContext, $model = null)
{
return Yii::createObject(['class' => Item::className(), 'itemArray' => $itemArray, 'lang' => $langContext]);
return new Item(['itemArray' => $itemArray, 'lang' => $langContext, 'model' => $model]);
}

/**
Expand Down
57 changes: 55 additions & 2 deletions modules/cms/src/menu/QueryIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

namespace luya\cms\menu;

use Yii;
use Iterator;
use yii\base\Object;
use luya\cms\models\Nav;
use luya\helpers\ArrayHelper;

/**
* Iterator class for menu items.
Expand All @@ -13,7 +17,7 @@
*
* @author Basil Suter <basil@nadar.io>
*/
class QueryIterator extends \yii\base\Object implements Iterator
class QueryIterator extends Object implements Iterator
{
/**
* @var array An array containing the data to iterate.
Expand All @@ -31,14 +35,63 @@ class QueryIterator extends \yii\base\Object implements Iterator
*/
public $with = [];

/**
* @var boolean Whether all models for each menu element should be preloaded or not, on large systems with propertie access it
* can reduce the sql requests but uses more memory instead.
*/
public $preloadModels = false;

/**
* @internal
*/
public function init()
{
parent::init();

if ($this->preloadModels) {
$this->loadModels();
}
}

private $_loadModels = null;

/**
* Load all models for ghe given Menu Query.
*
*
* @return array An array where the key is the id of the nav model and value the {{luya\cms\models\Nav}} object.
*/
public function loadModels()
{
if ($this->_loadModels === null) {
$this->_loadModels = Nav::find()->indexBy('id')->where(['in', 'id', ArrayHelper::getColumn($this->data, 'nav_id')])->with(['properties'])->all();
}

return $this->_loadModels;
}

/**
* Get the model for a given id.
*
* If the model was not preloaded by {{loadModels}} null is returned.
*
* @param integer $id
* @return null|\luya\cms\models\Nav
*/
public function getLoadedModel($id)
{
return isset($this->_loadModels[$id]) ? $this->_loadModels[$id] : null;
}

/**
* Iterator get current element, generates a new object for the current item on accessing.s.
*
* @return \luya\cms\menu\Item
*/
public function current()
{
return Query::createItemObject(current($this->data), $this->lang);
$data = current($this->data);
return Query::createItemObject($data, $this->lang, $this->getLoadedModel($data['id']));
}

/**
Expand Down
11 changes: 11 additions & 0 deletions modules/cms/tests/src/menu/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,15 @@ public function testInOperatorWithContainers()

$this->assertSame(3, $in);
}

public function testPreloadModels()
{
$default = (new Query())->where(['container' => 'default'])->all();

$this->assertNull($default->getInnerIterator()->getLoadedModel(1));

$default = (new Query())->where(['container' => 'default'])->preloadModels()->all();

$this->assertNotNull($default->getInnerIterator()->getLoadedModel(1));
}
}

0 comments on commit 7651d01

Please sign in to comment.