1

I have a model called Area which contains a list of area names that I need to populate a dropdown. The list is translated using the Rainlab Translate plugin.

If I just do a straightforward Area::lists() then the list is not translated. However, if I do Area::get()->lists() then it is translated but one query is run on the rainlab_translate_attributes table for every single item in the dropdown, causing ~100 queries to run and a 1.5s request duration.

Model

<?php namespace Namespace\PluginName\Models;

use Model;

class Area extends Model
{
    public $implement = ['RainLab.Translate.Behaviors.TranslatableModel'];

    public $translatable = ['name'];

    // .... 
}

View

<div class="form-group {{ errors.first('location_id') ? 'has-error' }}">
    {{ form_label('area_id','Area') }}
    {{ form_select('area_id', {'': 'Select...'} + area, null, {'class': 'form-control', 'placeholder': 'Select...'}) }}
    <small class="text-danger" data-validate-for="area_id"></small>
</div>

Component Option #1 (fast query but items don't get translated)

public function areas() {
    return Area::lists('name','id');
}

Component Option #2 (items translated but ~100 queries and very slow)

public function areas() {
    return Area::get()->lists('name','id');
}

In other similar situations I would add public $with = ['relation'] but the rainlab_translate_attributes table doesn't seem to have a model that I could related the Area model to.

UPDATE

I have created the following functions in my Area.php model:

public static function listAreas()
{
    $areas = Cache::rememberForever("all:" . App::getLocale()  , function() {
        return self::
        whereNotNull('iso3166_2')
        ->get()
        ->toArray();
    });

    return  self::makeCollection( $areas ) ;
}

public static function makeCollection ( array $models = [] )
{
    return self::hydrate( $models );
}

...and then in my component, I have tried:

$areas = Area::listAreas(); <-- this reads cached data immedately

$areas->lists('name','id'); <-- this causes a new query to be generated for every item in the collection, here's an example of one query:

select * fromrainlab_translate_attributeswherelocale= 'th' andmodel_id= '1275' andmodel_type= 'Namespace\PluginName\Models\Area' limit 1

I have verified that App::getLocale() is correctly set as th

Joseph
  • 2,737
  • 1
  • 30
  • 56
  • have you considered caching your query ? – Raja Khoury Feb 04 '18 at 18:08
  • Unfortunately caching the query wouldn't help because it would only cache the original query (which is only taking 50ms anyway) and not the subsequent queries that are called automatically to handle the translations. – Joseph Feb 21 '18 at 14:19
  • Actually I am caching the model, its translated attributes and relations. I'm using Redis, for example you can prefix the active locale to the cache key and have multiple versions cached. – Raja Khoury Feb 21 '18 at 19:46
  • Added an asnwer hope it helps. Cheers – Raja Khoury Feb 21 '18 at 20:56
  • try loading your model with `translations` relation. `protected $with = ['translations'];` – Natwar Singh Mar 17 '21 at 15:36

3 Answers3

1

You need to fire JOIN manually I guess, there seems no functionality available for collection.

$locale = 'de';
$query = \HardikSatasiya\DemoTest\Models\Relation::query();

$query->select($query->getModel()->getTable() .'.*');
$query->addSelect('rainlab_translate_attributes.attribute_data');
$query->leftJoin('rainlab_translate_attributes', function($join) use ($locale, $query) {
    $join
        ->on(\Db::raw(\DbDongle::cast($query->getModel()->getQualifiedKeyName(), 'TEXT')), '=', 'rainlab_translate_attributes.model_id')
        ->where('rainlab_translate_attributes.model_type', '=', get_class($query->getModel()))
        ->where('rainlab_translate_attributes.locale', '=', $locale)
    ;
});

$data = $query->get();
$translatedArray = [];
foreach ($data as $value) {
    if(is_null($value->attribute_data)) {
        $translatedArray[$value->id] = $value->name;
    }
    else {
        $translations = json_decode($value->attribute_data);
        $translatedArray[$value->id] = $translations->name;
    }

}
dd($translatedArray);

may be this will help you.

Hardik Satasiya
  • 9,547
  • 3
  • 22
  • 40
  • Hello again @Hardik Satasiya thank you for replying to my issue and sorry for the delay in replying. Your solution looks interesting but I am unsure about where to implement this code to use it in the front end. Does it belong in the component or model? – Joseph Feb 21 '18 at 14:21
  • I guess you can write this code inside `public static function listAreas(){ ..here...}` and return translated options. Caching is ok but it can be useful for static data, data which can be changed at some time may create issue sometimes not all time :) – Hardik Satasiya Feb 23 '18 at 07:36
  • 1
    I think that in this particular case (given that I am just dealing with a list of areas that will only change if the government reassigns the district names) caching is the best option. However, your suggestion will be highly valuable for other cases and I will keep a note of it. I have also upvoted it. Thank you very much for your help @Hardik Satasiya ! – Joseph Feb 23 '18 at 11:08
1

I had the same requirements and solved it using caching . If you don't want to cache your queries ignore this answer but I think you should consider it.

1) Make sure your RainLab Translator is configured, so when using App::getLocale() returns the Translator's active locale not Laravel's.

2) Create a method in your model for front-end usage. The purpose is to cache the Model / Relations & Translated Attributes.

E.g AreaModel.php

public static function listAreas()
{
       $areas = Cache::tags([  'areas' ])
            ->rememberForever(  "all:" . App::getLocale()  , function() {
                return self::
                    with(['relation_model_name']) // Fetch the Relation
                    ->get()
                    ->toArray();
            });

    return  self::makeCollection( $areas ) ;
}

public static function makeCollection ( array $models = [] )
{
    return self::hydrate( $models );
}

a) Here we are cahcing the query with a key that includes the active locale

b) We are adding with for related models

c) We just cache the whole collection ( No pluck / lists ) and converting back to eloquent model instance.

The advantages is that now in your component Area::listAreas(); will return the cached collections and you can manipulated like any other.

$areas = Area::listAreas(); // collection ( Area + Relation )

$dropdown = $areas->pluck('name', 'id'); // get Dropdown values for Areas...

Some Consideration is to clear the cache ( Delete Cache Tag / Key ) every time a record is Updates, Added or Deleted ( Model + Relations ) .

Below Screenshots of Redis Cache Store for a Store Model and its relation model Business Type;

E.g of Store Model - EN E.g of Store Model - ES

Update :

First I apologize for using Redis in my example assuming everyone does. The Original post should have been more focused on the implementation. Please don't copy / paste code as I did.

a) In my original answer I posted code using the hydrate() method to create an exisiting model instance from the cached records. It was confusing & not necesary but I doubt it has to do with the related queries to the rainlab translate. ( Needs confirmation )

b) return self::whereNotNull('iso3166_2')->get()->lists('name','id') is more than enough to cache Areas records.

c) In my comment I used pluck because lists is deprecated. pluck returns a collection - See here and here

$areas = self::whereNotNull('iso3166_2')->pluck('name', 'id') ; // collection
$areas->toArray();

I haven't tried the file-based caching yet with October and not sure about its behavior Vs Redis.

Again, Some Considerations ;

a) Please name your cache key to something unique and meaningful, in my post all + locale was related to a cache tag areas . e.g. areas.iso3166_2.locale ( Avoid overrides )

b) Add Cache::forget('key'); in your models afterSave & afterDelete methods

c) If you are caching related models also be careful to clear the cache when they change.

Raja Khoury
  • 3,015
  • 1
  • 20
  • 19
  • thank you for this -- I have it partially working in that I can call `$areas = Area::listAreas();` and it caches it the first time and then ready it from the cache the second time. I have verified that the correct translated values are present too. However, the unexpected behaviour is that if I run `$areas->pluck('name', 'id');` or `$areas->lists('name', 'id');` then it is automatically queries the rainlab translation table again. I don't understand why it is doing this when the data is cached. – Joseph Feb 22 '18 at 10:51
  • Are you sure the query is related to the Area Model ? Did you check you're not calling the model from other code ? Can you post your `listAreas` method ? – Raja Khoury Feb 22 '18 at 11:26
  • thank you for your reply -- I have updated the original question to show the code I have tried. Please note that I am using file-based cache and not redis so I had to remove the tags part. – Joseph Feb 22 '18 at 11:55
  • What if you just do return self::whereNotNull('iso3166_2')->pluck('name','id'): in your method and forget about 'hydrate' I posted it in my answer but it's not necessary for this case it was an example. – Raja Khoury Feb 22 '18 at 12:27
  • Ok, that works! I had to leave `get()` in there as well though, otherwise the translation didn't happen. So I did `return self::whereNotNull('iso3166_2')->get()->lists('name','id')` inside the `Cache` closure and it works perfectly. Thank you! Do you want to update the answer so that I can mark it correct? – Joseph Feb 22 '18 at 13:13
  • @Joseph Glad it helps - Answer Updated. You actually did most of the work. Good luck! – Raja Khoury Feb 22 '18 at 20:24
  • 1
    Thank you for updating the answer and for adding the reminder about clearing the cache on `afterSave` and `afterDelete` - I have marked the answer correct now. – Joseph Feb 23 '18 at 07:32
0

Another idea that could affect speed (if not number of queries) - indexing model's name property:

public $translatable = [
    ['name', 'index' => true]
];

Reference: https://github.com/rainlab/translate-plugin#indexed-attributes

Eoler
  • 131
  • 2
  • 4
  • Thank you, but unfortunately adding the index does not appear to have affected the speed of the queries. – Joseph Feb 21 '18 at 14:24