Reputation: 13110
I have the following tables:
Where a piece of content has_many categories, and a category is also a piece of content.
In content I have the following code:
public function getCategories()
{
return $this
->hasMany(Category::className(), ['id' => 'category_id'])
->viaTable('content_category', ['content_id' => 'id']);
}
public function getCategoriesCsv(){
...
}
For my grid view in the backend, I'd like to display a comma separated list of categories for each piece of content.
I'm aware that I could select this information separately, but I would like to do it as part of the find query and using the existing relation if possible.
Upvotes: 0
Views: 442
Reputation: 13110
WITH CATEGORIES LOADED
Originally I implemented it as:
public function getCategoriesCsv(){
$categoryTitles = [];
foreach ($this->categories as $category){
$categoryTitles[] = $category->title;
}
return implode(', ', $categoryTitles);
}
Thanks to @IStranger, I neatened this to:
public function getCategoriesCsv()
{
$titles = ArrayHelper::getColumn($this->categories, 'title');
return implode(', ', $titles);
}
WITHOUT CATEGORIES LOADED
I have now managed to avoid loading all the category models by adding a separate CategoryCsv ActiveRecord:
class CategoryCsv extends ActiveRecord
{
public static function tableName(){
return '{{%content_category}}';
}
public function attributes(){
return ['content_id', 'value'];
}
public static function find(){
return parent::find()
->select([
'content_id',
'GROUP_CONCAT(
categoryCsv.title
ORDER BY categoryCsv.title
SEPARATOR ", "
) value'
])
->innerJoin('content categoryCsv','category_id = categoryCsv.id')
->groupBy('content_id');
}
}
Then in the Content ActiveRecord:
public function getCategoriesCsv(){
return $this->hasOne(CategoryCsv::className(), ['content_id' => 'id']);
}
Thus I can access the value like so:
$contents = Content::find()->with('categoryCsv')->all();
foreach($contents as $content){
echo $content->categoryCsv->value;
}
Upvotes: 0
Reputation: 2041
This approach typical way and it works with related Category
models. Therefore it requires a lot of memory.
class Content extends \yii\db\ActiveRecord
{
/**
* Returns comma separated list of category titles using specified separator.
*
* @param string $separator
*
* @return string
*/
public function getCategoriesCsv($separator = ', ')
{
$titles = \yii\helpers\ArrayHelper::getColumn($this->categories, 'title');
return implode($separator, $titles);
}
// ...
}
Should be used with eager loading:
Content::find()
->with('categories')
->all();
This approach uses subqueries and don't use relations and related models. Therefore this way more fast and keeps a lot of memory.
class Content extends \yii\db\ActiveRecord
{
const ATTR_CATEGORIES_CSV = 'categoriesCsv';
/**
* @var string Comma separated list of category titles.
*/
public $categoriesCsv;
/**
* Returns DB expression for retrieving related category titles.
*
* @return \yii\db\Expression
*/
public function prepareRelatedCategoriesExpression()
{
// Build subquery that selects all category records related with current content row.
$queryRelatedCategories = Category::find()
->leftJoin('{{%content_category}}', '{{%content_category}}.[[category_id]] = {{%category}}.[[id]]')
->andWhere(new \yii\db\Expression('{{%content_category}}.[[content_id]] = {{%content}}.[[id]]'));
// Prepare subquery for retrieving only comma-separated titles
$queryRelatedCategories
->select(new \yii\db\Expression('GROUP_CONCAT( {{%category}}.[[title]] )'));
// Prepare expression with scalar value from subquery
$sqlRelatedCategories = $queryRelatedCategories->createCommand()->getRawSql();
return new \yii\db\Expression('(' . $sqlRelatedCategories . ')');
}
// ...
}
When alias of additional column equals to some model property, it will be populated by all()
method:
$contentModels = Content::find()
->andSelect([
'*',
Content::ATTR_CATEGORIES_CSV => Content::prepareRelatedCategoriesExpression(),
])
->all();
foreach ($contentModels as $contentModel) {
$contentModel->id;
$contentModel->categoriesCsv; // it will be also populated by ->all() method.
// ...
}
ps: I not tested this code, probably should be fixed query for retrieving categories.
Moreover, in this example it written using base simple syntax, but it may be optimized to more cute state using various helpers, junction models etc.
Upvotes: 1