Блог Андрея

 
 

Используем кэш для запросов к базе данных в Symfony 3/4

Начав изучать Symfony 3 я довольно быстро заметил, что один и тот же запрос к базе данных выполняется два раза подряд.

Например, вот такой метод контроллера

    /**
     * @Route("/training/countqueries", name="training_countqueries")
   */
    public function testCountQueries()
    {
        //Нужны:
        //SELECT * FROM main WHERE people = 1
	    //SELECT * FROM main WHERE people = 1

        /** @var Doctrine\ORM\EntityRepository $oRepository  */
        $oRepository = $this->getDoctrine()->getRepository('App:Main');
	    $oQueryBuilder = $oRepository->createQueryBuilder('m');
	    $oQueryBuilder->where($oQueryBuilder->expr()->eq('m.people', 1));

        //Отправляем два одинаковых запроса подряд
        $aResult = $oQueryBuilder->getQuery()->execute();
	    $aResult2 = $oQueryBuilder->getQuery()->execute();

        return $this->render('empty.html.twig', ['res' => $aResult2]);
    }

после запуска выполняет ровно два одинаковых запроса к базе данных. Глядя на этот пример вы можете подумать, а зачем выполнять два одинаковых запроса подряд и никто никогда не станет писать такой код, но поверьте, написать сайт, который неявно будет делать именно так очень легко. Пример для интересующихся, как это так может получиться приведу позже в отдельной статье.

Избежать беды можно используя так называемый в Doctrine 2 «кэш второго уровня».

Проект Symfony 3.4 на примере которого я описываю использование кэша второго уровня создан из консоли с помощью Symfony cli 4.3, это может быть важно, так как может влиять на пути к некоторым упомянутым файлам.

Для начала использования кэша второго уровня надо указать в конфигурации Symfony, что мы хотим его использовать.

Файл config/packages/doctrine.yaml.

В секцию doctrine добавляем:

second_level_cache:
            enabled: true

Далее, во все интересующие нас модели (я думаю, что это будут все модели моего проекта без исключения), добавляем аннотацию:

/**  @ORM\Cache(usage="NONSTRICT_READ_WRITE") */

Варианты значений usage можно посмотреть в документации Doctrine 2:

Режим кеширования

  • READ_ONLY (DEFAULT)
    • Может выполнять чтение, вставку и удаление, не может выполнять обновления или использовать какие-либо блокировки.
    • Полезно для данных, которые часто читаются, но никогда не обновляются.
    • Лучший исполнитель.
    • Это просто.
  • NONSTRICT_READ_WRITE
    • Read Write Cache не использует никаких блокировок, но может выполнять чтение, вставку, обновление и удаление.
    • Хорошо, если приложению нужно обновлять данные редко.
  • READ_WRITE
    • Кэш Read Write использует блокировки перед обновлением / удалением.
    • Используйте, если данные должны быть обновлены.
    • Самая медленная стратегия.
    • Для его использования реализация области кэша должна поддерживать блокировку.

Далее, переписываем наш метод таким образом:

    /**
     * @Route("/training/countqueries", name="training_countqueries")
    */
    public function testCountQueries()
    {
        //SELECT * FROM main WHERE people = 1
	    //SELECT * FROM main WHERE people = 1
        /** @var \Doctrine\ORM\EntityRepository $oRepository  */
        $oRepository = $this->getDoctrine()->getRepository('App:Main');
	    $oQueryBuilder = $oRepository->createQueryBuilder('m');
        $oQueryBuilder->setCacheable(true);
	    $oQueryBuilder->where($oQueryBuilder->expr()->eq('m.people', 1));
		
        $aResult = $oQueryBuilder->getQuery()->execute();
	    $aResult2 = $oQueryBuilder->getQuery()->execute();
	    //var_dump($aResult2[0]->getTitle());die;
        return $this->render('empty.html.twig', ['res' => $aResult2]);
    }

Видим, что выполняется один запрос к базе данных. Работает хорошо, но не нравится необходимость всякий раз вызывать setCacheable(true).

Так как мой проект Symfony пока ещё совсем свежий, я был готов использовать собственный EntityManager, создав его так, как описано здесь и здесь.

Но оказалось, существует более простое решение, достаточно использовать вместо QueryBuilder::getQuery::execute() другой класс для работы с данными Doctrine\Common\Collections\Criteria и метод RepositoryEntity::matching.

    /**
     * @Route("/training/criteria/countqueries", name="training_criteria_countqueries")
    */
    public function testCriteriaCountQueries()
    {
		//need
        //SELECT * FROM main WHERE people = 1
		//SELECT * FROM main WHERE people = 1
		/** @var Doctrine\ORM\EntityRepository $oRepository */
		$oRepository = $this->getDoctrine()->getRepository('App:Main');
		//$oRepository->
		
		$oCriteria = Criteria::create();
		$e = Criteria::expr();
		$oCriteria->where($e->eq('people', 1));
		/** @var Doctrine\ORM\LazyCriteriaCollection $aResult */
		$aResult = $oRepository->matching($oCriteria);
		//$aResult->
		$aResult->get(0);
		
		$oCriteria2 = Criteria::create();
		$e2 = Criteria::expr();
		$oCriteria2->where($e2->eq('people', 1));
		$aResult2 = $oRepository->matching($oCriteria2);
		$aResult2->get(0);
		
		return $this->render('empty.html.twig', ['res' => $aResult]);
	}

Запрос выполняется один раз при наличии аннотации ORM\Cache о которой уже писал выше.

Помимо ответа на вопрос, вынесенный в тайтл статьи я наконец смог для себя определиться, что же предпочтительнее использовать, QueryBuilder или Criteria.

Использовать Criteria и EntityRepository::matching предпочтительнее, так как это снижает риск нагрузить базу данных.

Возможно, вам также будет интересно использование memcache для хранения результатов sql запроса в Symfony 3.