Как правильно составить запрос в поисковой модели yii2?

Доброе утро.
Есть такая задача:

Пользователь может выбрать до 5­ти ингредиентов для приготовления блюда, при
этом:
1. Если найдены блюда с полным совпадением ингредиентов ­ вывести
только их.
2. Если найдены блюда с частичным совпадением ингредиентов ­ вывести
в порядке уменьшения совпадения ингредиентов вплоть до 2­х.
3. Если найдены блюда с совпадением менее чем 2 ингредиента или не
найдены вовсе ­ вывести “Ничего не найдено”.
4. Если выбрано менее 2­х ингредиентов ­ не производить поиск, выдать
сообщение: “Выберите больше ингредиентов”.

Создал три таблицы:
1) Recipes // таблица рецептов
2) Ingredients // таблица ингредиентов
3) RecipesIngredients // связующая таблица
Вот миграции таблиц:
Migrations

class m170818_200201_ingredient_table extends Migration
{
    public function safeUp()
    {
        $tableOptions = null;

        if($this->db->driverName == 'mysql'){
            $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
        }

        $this->createTable('{{%ingredients}}',[
            'id' => $this->primaryKey(),
            'name' => $this->string()->notNull(),
            'created_at' => $this->integer()->notNull(),
            'updated_at' => $this->integer()->notNull(),
            'status' => $this->smallInteger()->notNull()->defaultValue(0)
        ], $tableOptions);

        $this->createIndex('idx-ingred-id', '{{%ingredients}}', 'id');
        $this->createIndex('idx-ingred-name', '{{%ingredients}}', 'name');
        $this->createIndex('idx-ingred-status', '{{%ingredients}}', 'status');


    }

    public function safeDown()
    {
        $this->dropTable('{{%ingredients}}');
    }
}

class m170818_200136_recipes_table extends Migration
{
    public function safeUp()
    {
        $tableOptions = null;
        if($this->db->driverName == 'mysql'){
            $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
        }

        $this->createTable('{{%recipes}}',[
            'id' => $this->primaryKey(),
            'name' => $this->string(100),
            'created_at' => $this->integer()->notNull(),
            'updated_at' => $this->integer()->notNull(),
            'status' => $this->smallInteger()->notNull()->defaultValue(0)
        ], $tableOptions);

        $this->createIndex('idx-recipe-id', '{{%recipes}}', 'id');
        $this->createIndex('idx-recipe-name', '{{%recipes}}', 'name');
        $this->createIndex('idx-recipe-status', '{{%recipes}}', 'status');
    }

    public function safeDown()
    {
        $this->dropTable('{{%recipes}}');
    }
}

class m170819_172758_recipes_ingredients_table extends Migration
{
    public function safeUp()
    {
        $this->createTable('{{%recipes_ingredients}}', [
            'recipe_id' => $this->integer()->notNull(),
            'ingredient_id' => $this->integer()->notNull()
        ]);

       $this->addPrimaryKey('pk-recipes-ingredients', '{{%recipes_ingredients}}', ['recipe_id', 'ingredient_id']);

        $this->createIndex('idx-recipe', '{{%recipes_ingredients}}', 'recipe_id');
        $this->createIndex('idx-inged', '{{%recipes_ingredients}}', 'ingredient_id');

        $this->addForeignKey('fk-recipe_id', '{{%recipes_ingredients}}', 'recipe_id', '{{%recipes}}', 'id', 'CASCADE', 'RESTRICT');
        $this->addForeignKey('fk-ingred_id', '{{%recipes_ingredients}}', 'ingredient_id', '{{%ingredients}}', 'id', 'CASCADE', 'RESTRICT');

    }

    public function safeDown()
    {
        $this->dropTable('{{%recipes_ingredients}}');
    }
}


Мои попытки самостоятельно решить данную задачу завершились на такой поисковой модели:
Model

<?php
namespace app\modules\recipes\models\search;

use app\modules\recipes\models\Ingredients;
use app\modules\recipes\models\RecipesIngredients;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\modules\recipes\models\Recipe;

class RecipesIngredientsSearch extends RecipesIngredients
{
    private $_id = [];
    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['recipe_id', 'ingredient_id'], 'integer'],
        ];
    }

    /**
     * @inheritdoc
     */
    public function scenarios()
    {
        // bypass scenarios() implementation in the parent class
        return Model::scenarios();
    }

    /**
     * Creates data provider instance with search query applied
     *
     * @param array $params
     *
     * @return ActiveDataProvider
     */
    public function search($params)
    {

        foreach($params['Ingredients'] as $key => $value){

            if(!empty($value['name'])){
                $this->_id[] = $value['name'];
            }
        }

        $query = RecipesIngredients::find();
        $query->joinWith(['ingredient', 'recipe'])->where(['ingredients.status' => Ingredients::STATUS_ACTIVE, 'recipes.status' => Recipe::STATUS_ACTIVE]);
        $query->andWhere(['in', 'recipes_ingredients.ingredient_id', $this->_id]);
        $query->orderBy(['recipes.name' => SORT_ASC]);
        $query->addOrderBy(['recipes.id' => SORT_DESC]);

        // add conditions that should always apply here

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
        ]);

        $this->load($params);

        if (!$this->validate()) {
            // uncomment the following line if you do not want to return any records when validation fails
            // $query->where('0=1');
            return $dataProvider;
        }

        return $dataProvider;
    }
}


P.S Так же в моделях есть связи
Recipe
public function getIngred()
    {
        return $this->hasMany(Ingredients::className(), ['id' => 'ingredient_id'])->viaTable('{{%recipes_ingredients}}', ['recipe_id' => 'id']);
    }

Ingredients
public function getRecipe()
    {
       return $this->hasMany(Recipe::className(), ['id' => 'recipe_id'])->viaTable('{{%recipes_ingredients}}', ['ingredient_id' => 'id']);
    }

RecipesIngredients
public function getRecipe()
    {
        return $this->hasOne(Recipe::className(), ['id' => 'recipe_id']);
    }

    public function getIngredient()
    {
        return $this->hasOne(Ingredients::className(), ['id' => 'ingredient_id']);
    }

Подскажите, как решить данную задачу?
  • Вопрос задан
  • 427 просмотров
Пригласить эксперта
Ответы на вопрос 2
webinar
@webinar Куратор тега Yii
Учим yii: https://youtu.be/-WRMlGHLgRg
Мне кажется надо было пойти простым путем. Так как у Вас в задаче количество ингредиентов ограничено, то можно не делать такую сложную структуру. Если бы их было неизвестное количество, то да, а так Вам заведомо известно, что их 5 и более не будет. Так что оптимальнее сделать так:
Recipes: id | name | ingrigient_0| ingrigient_1| ingrigient_2| ingrigient_3| ingrigient_4
ingrigients: id | name
И хранить в ingrigient_* id ингридиента
Будет и код проще и запросы легче и т.д.
Как вариант можно было бы и одно поле с json, в котором имена ингридиентов, последний mysql имеет соответствующий тип поля, но тут пока руки не дошли разобраться с этим, так что почитайте прежде чем принимать решение.
Ответ написан
DaFive
@DaFive
SELECT r.id, COUNT(ri.recipe_id) AS count_ingredients
FROM recipes AS r
LEFT JOIN recipes_ingredients AS ri ON r.id = ri.recipe_id
GROUP BY r.id

Будет вывод рецепта с количеством ингредиентов. Можете добавить HAVING count_ingredients > _число_ для выборки. Просто вам если через модели надо - то решение такое же как через CDbCommand, только используя Model::find()->select('.....').
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы