I use TranslationBehavior to handle translated data in my app. When retrieving data, CakePHP returns the translated string or if this string is not available, the value of the default locale. To request the data, the following query is used:
$items = $this->Model->find('all')
->all()
->extract('name');
To order the output, the query is:
$items = $this->Model->find('all')
->order([$this->Model->translationField('name') => 'ASC'])
->all()
->extract('name');
This works for all default locale items and all translated items. But when the translation for a record is missing, it will break the order. In this case the correct fallback value is returned, but the order is no longer correct. The output looks like this:
['A... (Translated)', 'B... (Translated)', 'A... (Default)', 'C... (Default)']
What I expect is the following order:
['A... (Default)', 'A... (Translated)', 'B... (Translated)', 'C... (Default)']
To achieve this, I changed the query to:
$items = $this->Model->find('all')
->order(['IF('.$this->Model->translationField('name').' != "", '.$this->Model->translationField('name').', Model.name)' => 'ASC'])
->all()
->extract('name');
Which gives the expected order:
['A... (Default)', 'A... (Translated)', 'B... (Translated)', 'C... (Default)']
The question is: is this the correct way to handle the order of mixed locales? Or did I miss something and CakePHP already provides a simpler solution?
You may ask, why should we mix the locales? In my case, it's not necessary to translate all the strings, because some of the items are identical in both languages.
Looking at how CakePHP queries the translated fields, and merges them later on at PHP level, this is the expected behavior, and you'd indeed have to use a conditional expression in the ORDER clause.
I'd suggest to use a CASE expression, as the IF() function is MySQL specific, something like:
$query = $this->Model->find();
$query
->orderAsc(
$query->newExpr()->addCase(
[
$query->newExpr()->isNotNull($this->Model->translationField('name'))
],
[
$query->identifier($this->Model->translationField('name')),
$query->identifier('Model.name')
]
)
)
// ...
Which would generate an expression similar to:
ORDER BY
CASE WHEN Model_name_translation.content IS NOT NULL
THEN Model_name_translation.content
ELSE Model.name END
ASC
And as mentioned in the comments, If you go the "do not translate identical strings" route, then you should avoid storing empty strings for such "missing" translations, instead do not store a record for them at all.
In order to avoid all this you could of course add translations for everything, even if they are equal in the different languages.
Related
I cant seem to get the filter clause to retrieve documents from my index using a regex clause. The schema for my index is straight forward, I only have a single field which is both searchable and filterable and is of type Edm.String, called someId (which normally contains a hash value of something) and has sample values like:
someId
k6l7k2oj
k6l55iq8
k6l61ff8
...
I need to be able to extract all values from this field that start with K6 and end with 8. So based on the documentation I am using this in my POST request body
{
"filter": "search.ismatch('/^k6[?]*d$/','someId','simple','all')",
"select":"someId",
"count":"true"
}
and it comes up with nothing.
On the other hand if I simplify and say I only need data where someId starts with K6, I seem to get some success if i just use a wild card.
like this:
{
"filter": "search.ismatch('k6l*','someId','simple','all')",
"select":"someId",
"count":"true"
}
I do get what I am looking for. Question is why does the regex not work with search.isMatch(), what am i missing?
...
Regex is part of the full Lucene syntax; It is not available in the simple syntax. Try changing the third parameter of search.ismatch to 'full'.
Also, did you mean to use search.ismatch or search.ismatchscoring? The latter is functionally equivalent to using the top-level search, searchFields, queryType, and searchMode parameters. The former does not count matches towards relevance scoring.
Your regex does not do what you intend either it seems. I tested your regex with your sample data and it does not match. Try this regex instead:
^k6.{5}8$
It matches a lowercase k6 from the start of the string, followed by 5 characters of anything and finally an 8.
Complete example
{ "filter": "search.ismatch('^k6.{5}8$','someId','full','all')", "select":"someId", "count":"true" }
Thanks to Dan and Bruce.
This exact expression worked for me
{
"filter": "search.ismatch('/k6.{5}8/','someId','full','all')",
"select":"someId",
"count":"true"
}
I am generating a case statement to return either 0 or 1:
$desc_case = $q->newExpr()
->addCase(
[$q->newExpr()->add(["description IS" => NULL])],
[0,1],
["integer","integer"]
);
$q = $q->select(["has_desc" => $desc_case]);
Which results in the following correct SQL:
SELECT [fields removed for clarity], (CASE WHEN (description) IS NULL THEN :c0 ELSE :c1 END) AS `has_desc` FROM skills Skills
I've turned hydration off, and retrieved the result with
->hydrate(false)->toArray();
The result of the CASE statement is returned as a string- either "0" or "1" - which is messing up logic downstream.
I've traced the execution code as best I can, and it looks like CakePHP is using the type names passed to correctly bind the values, but nowhere does the type make its way into the TypeMap used for mapping output.
An easy workaround is to adjust the values after the fact (which I'm doing), but I'd like to make this work as expected on principle... :)
The type information passed to addCase() is only ment to be used for input casting, ie the values passed will be bound as the given types. The return values, ie the values being selected via the compiled CASE statement, are in no way being affected.
If you want to affect the type used for casting selected values of columns that do not exist in the schema (note that changing the schema will also affect other parts of the ORM and the query builder), then you have to change the type map accordingly, for example:
$query
->getSelectTypeMap()
->addDefaults([
'has_desc' => 'integer'
]);
See also
Cakephp-3.x: How to change the data type of a selected alias?
I'm trying to get a query working using a case statement, and can't figure out how to get the case to return a column value instead of a constant. I have the query working perfectly, except that the column names I'm providing for the results are being quoted or otherwise mishandled by Cake or maybe PDO somewhere down in a layer that I can't dig my way through. I got as far down as bindValue, but none of the documentation I encountered along the way tells me how to do this.
I have found this example comment:
$statement->bindValue(1, 'a title');
$statement->bindValue(2, 5, PDO::INT);
$statement->bindValue('active', true, 'boolean');
$statement->bindValue(5, new \DateTime(), 'date');
but in all these cases, the value provided is a constant. I need to pass in a string that is the name of the column that I want returned.
I tried both 'string' (resulted in quoted column name) and 'integer' (resulted in 0). I tried PDO::FETCH_COLUMN (seemed highly unlikely, but looked like the next best bet from http://php.net/manual/en/pdo.constants.php, and easy to try it...). I tried 'literal', inspired by the way you can put literal strings into expressions (resulted in Error: unknown type "literal"). That error message led me to src/Database/Type.php, but nothing in there helped me either.
So, I'm pretty much stumped. Here's a simple version of the code I have (leaving out a couple of conditions and unrelated columns):
$query = $this->Games->find();
$team_id = $query->newExpr()->addCase(
[$query->newExpr()->eq('Games.status', 'home_default')],
['home_team_id', 'away_team_id'],
['string', 'string']
);
$defaulting = $query
->select([
'id' => $team_id,
'count' => 'COUNT(Games.id)',
])
->where([
'Games.status IN' => ['home_default', 'away_default'],
])
->group('id')
->toArray();
This generates this SQL:
SELECT
(CASE WHEN Games.status = 'home_default'
THEN 'home_team_id' ELSE 'away_team_id' END) AS `id`,
COUNT(Games.id) AS `count`
FROM games Games
WHERE Games.status in ('home_default','away_default')
GROUP BY id
Note that THEN 'home_team_id' ELSE 'away_team_id' END should be simply THEN home_team_id ELSE away_team_id END. This will then allow me to read the list of ids of teams that have defaulted games along with the number of games they defaulted.
By default the values passed to the second argument of QueryExpression::addCase() are being treated as to be converted to literal values, not as identifiers. If you need the latter, then you should use an expression, an IdentifierExpression.
use Cake\Database\Expression\IdentifierExpression;
// ...
$team_id = $query->newExpr()->addCase(
[
$query->newExpr()->eq('Games.status', 'home_default')
],
[
new IdentifierExpression('Games.home_team_id'),
new IdentifierExpression('Games.away_team_id')
]
);
Also ditch the third argument in this case, you don't want the values to be string literals (for expressions the types would be ignored anyways).
1) Finding by instance object
Assuming I have the instance object called #topic. I want to retrieve the answers for this given topic. I was thinking I should be able to pass in :topics=>#topic, but i had to do the very ugly query below.
#answers = Answers.where(:topic_ids => {"$in" => [#topic.id]})
2) Getting the string representation of the id. I have a custom function (shown below). But shouldn't this be a very common requirement?
def sid
return id.to_s
end
If your associations are set up correctly, you should be able to do:
#topic.answers
It sounds like the above is what you are looking for. Make sure you have set up your associations correctly. Mongoid is very forgiving when defining associations, so it can seem that they are set up right when there is in fact a problem like mismatched names in references_many and referenced_in.
If there's a good reason why the above doesn't work and you have to use a query, you can use this simple query:
#answers = Answer.where(:topic_ids => #topic.id)
This will match any Answer record whose topic_ids include the supplied ID. The syntax is the same for array fields as for single-value fields like Answer.where(:title => 'Foo'). MongoDB will interpret the query differently depending on whether the field is an array (check if supplied value is in the array) or a single value (check if the supplied value is a match).
Here's a little more info on how MongoDB handles array queries:
http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-ValueinanArray
I have been trying to figure out how to do this and it seems that its not something that many people are trying to do in cakephp or I am just completely misunderstanding the documentation.. I am using the following query below to get a field value so I get a value where the "findbycreated" is 0... this part works fine
$unregisteredemail = $this->Testmodel->findBycreated('0');
$emailaddress = $unregisteredemail['Testmodel']['emailaddress'] ;
$emailpassword = $unregisteredemail['Testmodel']['password'] ;
But now, after I do some things with this data that I retrieved, I want to mark a field, in the same row, in the same model / table as a value of '1' to indicate that an action has taken place (email address has been successfully created, for example)... I just can't figure out how to do this in cakephp despite my efforts of going through the documentation and searching, this should be rather simple, I am tempted, at this point, to just use a regular mysql query as its a simple query.. basically the query is (please excuse my syntax as I haven't used direct mysql queries in a while) "update (database / table) set 'created'='1' where 'emailaddress'=$emailaddress"
Or I could use the row ID, if needed, as cakephp seems to prefer this, but still can't get how to do this.. this is my attempt below that is not working:
// update database to show that email address has been created
$this->Testmodel->read('emailaddress', $this->Testmodel->findBycreated('0'))
$this->Testmodel->id = 1;
$this->Testmodel->set(array(
'created' => '1'
));
$this->Testmodel->save();
There are, as you can see from the previous answers, several ways to achieve the same end. I'd just like to explain a little about why your way didn't work.
In the model, CakePHP has abstracted the database row(s) into an array according its implementation of ORM . This provides us with a handy way of manipulating the data and chucking it around the MVC architecture.
When you say:
$this->Testmodel->set(array(
'created' => '1'
));
You are dealing directly with the model, but the data is actually stored, as an array, in a class variable called $data. To access and manipulate this data, you should instead say:
$this->data['Testmodel']['created'] => '1';
The reason for specifying the model name as the first index is that where associated tables have been retrieved, these can be accessed in the same way, so you might have , for instance:
Array([Testmodel] => Array ([id] => 1,
[created] => [1],
...
)
[Relatedmodel] => Array ([id] => 1,
[data] => asd,
...
)
)
...and so on. Very handy.
Now, when you use $this->MyModelName->save() with no parameters, it uses $this->data by default and uses the part of the array of data appropriate to the model you are calling the save method on. You can also pass an array of data, formatted in the same way if, for some reason, you don't (or can't) use $this->data.
Your use of the method read() is incorrect. The first parameter should be null, a string or an array of strings (representing fieldname(s)). The second parameter should be the id of the record you wish to read. Instead, for param 2, you are passing the result of a find, which will be an array. The result, which you are not capturing, will be empty.
I would write your code like:
$this->data = $this->Testmodel->read('emailaddress',1);
$this->data['Testmodel']['created'] = 1;
$this->Testmodel->save();
or more succinctly:
$this->Testmodel->id = 1;
$this->Testmodel->saveField('created', 1);
In this situation I would let Cake deal with the id's and just focus on changing the row data and resaving it to the database
$row = $this->Model->findBycreated(0);
$row['Model']['emailaddress'] = 1;
$this->Model->save($row);
This way, you don't have to worry about the id's, as the id will be in your dataset anyway, so just change what you want and then tell Cake to save it.
Ninja edit, Be sure that you are returning a full row with an id from your findBycreated() method.
There're many ways to do your work.I suggest you to read the cookbook about saving data in cakephp.And besides david's solution another simple way would be
$this->Testmodel->id = 1;
$this->Testmodel->saveField('created' =>'1');
Ok, I think I finally found the solution, I was able to get this to work:
$this->Test->updateAll(
array(
'Test.field' => 'Test.field+100'
),
array(
'Test.id' => 1
)
);
I think you have to use updateAll as anything else will just create a new row.. basically CakePHP, for whatever reason, neglected to include a function for updating just one field so you have to put it into an array with the updateAll to make it work...
the +100 is where the updated info goes, so in this case "100" would be what the field is updated to.
In cakephp 3.x the syntax seems to be different. This is what worked for me in 3.x:
$this->Tests->updateAll(
[
'Tests.field = Tests.field+100'
],
[
'Tests.id' => 1
]
];
The difference is that the entire expression needs to be in the value of the first array.