Yii2 eager load aggregation through junction table - mysql

I have the following tables:
content - id (PK), title, ... other fields ...
content_category - content_id (FK to content), category_id (FK to content)
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.

Using defined relation (more simple, less efficient).
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();
Using subquery (more efficient, less convenient)
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.

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;
}

Related

Select related rows in Yii2

I have related tables (table train_schedule have departute_station_id and arrival_station_id) linked on Station:
enter image description here
I want select records from train_schedule with stations name:
$trainsTchedule = TrainSchedule::find()
->joinWith('getArrivalStation')
->joinWith('getDepartuteStation()')
->all();
In TrainSchedule class:
/**
* #return \yii\db\ActiveQuery
*/
public function getArrivalStation()
{
return $this->hasOne(Station::className(), ['id' => 'arrival_station_id']);
}
And
/**
* #return \yii\db\ActiveQuery
*/
public function getDepartuteStation()
{
return $this->hasOne(Station::className(), ['id' => 'departute_station_id']);
}
Error:
Relation names are case sensitive. app\models\TrainSchedule has a
relation named "arrivalStation" instead of "ArrivalStation".
Howe get data wrom linked tables?
You have defined your relations correctly but, you are calling them incorrectly. Your relation
getArrivalStation should be specified as arrivalStation
getDepartuteStation() should be departuteStation
when specifying in the joinWith, and you need to provide an array if you need to specify multiple relations when calling joinWith as currently your second call ->joinWith('getDepartuteStation()') is overriding the previous one ->joinWith('getArrivalStation').
So the query should look like below
$trainsTchedule = TrainSchedule::find()
->joinWith(['arrivalStation','departuteStation'])
->all();
You should read about Working with Relational Data

PyroCMS(Laravel) where clause within the translations not working correctly

I have been struggling with this for quite a while. I use PyroCMS and it has a Posts module that has all the fields in the database and all that and if you want to find a specific post, you can just use a normal WHERE clause and find a post by a date and so on.
But if a field is checked in CMS as translatable, I can't access that field and use it to find a post, because the CMS creates another field in another table that is called posts_translations, and it contains all the fields that are translatable. Usually that is a simple $posts->where("field","value"), but the field doesn't exist if it's translatable.
So I tried to use whereHas, but it doesn't really return anything.
public function meklet(PostRepositoryInterface $posts, $q)
{
$postss = $posts->all()->whereHas('translations', function($query) use($q) {
$query = $query->where(function($query) use($q) {
$query->where('title', 'like', '%'.$q.'%');
});
});
die(var_dump($q));
return $this->view->make("mendo.module.report::reports/search");
}
As you can see I use PostRepositoryInterface maybe I need to use some other class to access what I want? Im very confused, I know its a laravel base, but I can't really wrap my head around this simple problem.
You shouldn't use one letter variables and too much nested functions there:
/**
* Searches for all matches.
*
* #param PostRepositoryInterface $posts The posts
* #param string $search The search
* #return View
*/
public function search(PostRepositoryInterface $posts, $search)
{
/* #var PostCollection $results */
$results = $posts->all()->filter(
function (PostInterface $post) use ($search) {
return str_contains(
strtolower($post->getFieldValue('title')),
strtolower($search)
);
}
);
dd($results);
return $this->view->make('mendo.module.report::reports/search', [
'posts' => $results,
]);
}
And route should be like:
'posts/search/{search}' => [
'as' => 'anomaly.module.posts::posts.search',
'uses' => 'Anomaly\PostsModule\Http\Controller\PostsController#search',
],
To use a DB query directly you need to write translations join self. It is not so difficult.

Yii2 Restful. How can i receive data from 3 tables

I want to receive data like this:
categories
----category1
----category2
topProducts
----product1
--------photo1
--------photo2
----product2
--------photo1
--------photo2
I need get all categories and top x products.
Each product has two photos.
How can i do this by using yii2 restful?
Thanks.
the query shold look something like this
Category::find()
->with(['subcategories','topProducts', 'topProducts.images'])
->all();
you can use joinWith if you absolutely want a single query
if you retrieve your data with an ActiveController, you need to specify extraFields to the Category model. (here's a rest-specific usage example - rest of the guide should prove usefull as well)
Category model:
public function extraFields() {
return ['subcategories', 'topProducts'];
}
// product relation
public function getTopProducts(){
return $this->hasMany(Product::className(), ['category_id' => 'id'])
// ->order()->where() // your criterias
->limit(10);
}
// subcategories
public function getChildren(){
return $this->hasMany(Category::className(), ['id' => 'parent_id']);
}
Product model:
public function extraFields() {
return ['iamges'];
}
public function getImages(){
return $this->hasMany(Image::className(), ['product_id' => 'id'])
}
ps. since you haven't posed any code or table structure, all relations in my example are based on standard naiming convention

Find object through chained ManyToOne relationships

I'm trying to figure out how to find a group of objects based on a layered relationship. I have 3 entities like so:
Referral --> manyToOne --> Patient --> manyToOne --> Payor
How do I find all referrals a given payor?
I'm using symfony3 with mysql and doctrine. My entities:
class Referral
{
// usual stuff
/**
* #ORM\ManyToOne(targetEntity="Patient")
*/
private $patient;
}
class Patient
{
// usual stuff
/**
* #ORM\ManyToOne(targetEntity="Payor")
*/
private $payor;
}
class Payor
{
// usual stuff
}
Obviously I could make the relationships birectional, for example so I could do something like this in my controller:
$patients = $payor->getPatients();
foreach ($patients as $patient) {
$referrals = $patient->getReferrals();
}
And then collect these into an appropriate array, but this seems messy and I'd rather do it all in a single database query in my repository. Can that be done?
you can find all referrals for a given payor using a query.
in ReferralRepository
public function findReferralsByPayor(Payor $payor)
{
$qb = $this->createQueryBuilder('r');
$qb
->join('BUNDLENAME:Patient', 'p', 'WITH', 'p.id = r.patient')
->where('p.payor = :payor')->setParameter('payor', $payor)
;
return $qb->getQuery()->getResult();
}

Yii2 Model search query

How can I add where condition to my Articles model so that slug(From category model) is equal to $slug?
And this is a function that Gii generated:
public function getCategory()
{
return $this->hasOne(Categories::className(), ['id' => 'category_id']);
}
Here's my code:
public function specificItems($slug)
{
$query = Articles::find()->with('category');
$countQuery = clone $query;
$pages = new Pagination(['totalCount' => $countQuery->count(),'pageSize' => 12]);
$articles = $query->offset($pages->offset)
->limit($pages->limit)
->all();
return ['articles' => $articles,'pages' => $pages];
}
Your SQL query should contain columns from both article and category table. For that you need to use joinWith().
$result = Articles::find()
->joinWith('category')
->andWhere(['category.slug' => $slug])
->all();
Where 'category' is then name of your category table.
However, in your code you deviate from certain best practices. I would recommend the following:
Have both table name and model class in singular (Article and article). A relation can be in plural, like getCategories if an article has multiple categories.
Avoid functions that return result sets. Better return ActiveQuery class. If you have a query object, all you need to get the actual models is ->all(). However, you can further manipulate this object, add more conditions, change result format (->asArray()) and other useful stuff. Returning array of results does not allow that.
Consider extending ActiveQuery class into ArticleQuery and implementing conditions there. You'll then be able to do things like Article::find()->joinWith('category')->byCategorySlug('foo')->all().