I am trying to get FuelPHP's orm to use a compound key when using the related() method.
I have a db setup with two tables and I'd like to relate them based on their compound keys.
For example I would normally use a relation like this in my orm model:
protected static $_has_one = [
'message_flag' => [
'key_from' => 'object_id',
'model_to' => '\ModelClass',
'key_to' => 'object_id',
'cascade_save' => false,
'cascade_delete' => false,
],
];
But instead of using key_from => 'id and key_to => 'object_id, I'd like to join them on their composite key, which might look something like this:
protected static $_has_one = [
'message_flag' => [
'key_from' => ['object_id', 'other_key'],
'model_to' => '\ModelClass',
'key_to' => ['object_id', 'other_key',
'cascade_save' => false,
'cascade_delete' => false,
],
];
To clarify, the top example, which is the one recommended in FuelPHP's docs, creates a query that looks like this:
SELECT `t0`.`object_id` AS `t0_c0`, `t0`.`other_key` AS `t0_c1` FROM `myTable` AS `t0` LEFT JOIN `otherTable` AS `t1` ON (`t0`.`object_id` = `t1`.`object_id`);
But the query that I'd like to build with the ORM would look like this:
SELECT `t0`.`object_id` AS `t0_c0`, `t0`.`other_key` AS `t0_c1` FROM `myTable` AS `t0` LEFT JOIN `otherTable` AS `t1` ON (`t0`.`object_id` = `t1`.`object_id` AND `t0`.`other_key` = `t1`.`other_key`);
Actually, I just tried my hopeful, but undocumented example above
protected static $_has_one = [
'message_flag' => [
'key_from' => ['object_id', 'other_key'],
'model_to' => '\ModelClass',
'key_to' => ['object_id', 'other_key',
'cascade_save' => false,
'cascade_delete' => false,
],
];
Surprised to say this actually worked. Still, if anyone else has a similar problem, I hope they find this answer.
Related
I can not use where in deeply nested associations.
I have tried in many ways, but in every case where I use where it doesn't take action
Actually, my case is like in Documentation
https://book.cakephp.org/3.0/en/orm/query-builder.html#using-innerjoinwith
What I have tried until now
$query = $itemTable
->find('all')
->contain(
[
'Products',
'ItemGroups',
'ItemGroups.FeaturesItemGroups.Features',
'Products.ProductCategories.ProductFractions',
'Products.ProductVessels',
'ItemCancellationReasons',
'Features',
'Users',
'Users.Groups',
'Orders',
'ItemPrices.ItemPricesBillingDocuments.RecyclerPrices.ProductFractions' => [
'conditions' => [
'ItemPricesBillingDocuments.id IS NOT NULL',
'ItemPricesBillingDocuments.billing_document_id' => $billingDocumentToRevert->id,
'ItemPricesBillingDocuments.billing_document_type_id' => BillingDocumentTypes::CREDIT_INDEX,
'ItemPricesBillingDocuments.active' => 1
]
]
]
);
Second example
->innerJoinWith(
'ItemPrices.ItemPricesBillingDocuments',
function ($q) use ($billingDocumentToRevert) {
return $q->where(
[
'ItemPricesBillingDocuments.id IS NOT NULL',
'ItemPricesBillingDocuments.billing_document_id' => $billingDocumentToRevert->id,
'ItemPricesBillingDocuments.billing_document_type_id' => BillingDocumentTypes::CREDIT_INDEX,
'ItemPricesBillingDocuments.active' => 1
]
)
->contain(['RecyclerPrices.ProductFractions']);
}
)
3.
$query->where([
'ItemPricesBillingDocuments.id IS NOT NULL',
'ItemPricesBillingDocuments.billing_document_id' => $billingDocumentToRevert->id,
'ItemPricesBillingDocuments.billing_document_type_id' => BillingDocumentTypes::CREDIT_INDEX,
'ItemPricesBillingDocuments.active !=' => false
]);
And as result in my debugging I am getting results where active flag is false.
Each relation from Items table is 1:many
ItemsPrices.ItemPricesBillingDocuments is also 1:many
If I'd have the following table structure
ID name type
1 John Admin
2 Johan Admin
3 Sam User
4 Rick User
How would I use Laravels Eloquent with a groupBy query, but still grab each record, and also sum up per type how many records each type has?
Example result would be as follow
[
'type' => 'Admin',
'records' => 2
'data' => [
0 => [
'name' => 'John'
],
1 => [
'name' => 'Johan'
]
]
],
[
'type' => 'User',
'records' => 2
'data' => [
0 => [
'name' => 'Sam'
],
1 => [
'name' => 'Rick'
]
]
]
Since you are querying all the users you should be doing the grouping after the query instead of grouping in query like:
$users = User::all()->groupBy('type');
Then you can do:
foreach ($allUsers as $type => $users) {
$type; // 'Admin'
$users->count(); // Record Count
foreach ($users as $user) {
$user->name; //
}
}
If you want the data EXACTLY like your example then it would be:
User::all()
->groupBy('type')
->map(function($users, $type) {
return [
'type' => $type,
'records' => $users->count(),
'data' => $users->pluck('name')
];
})
->values();
I think the best way to achieve this is to have a table with types related through hasMany() to your data.
Then you can use standard Eloquent tools without restructuring the data manually:
$data = Type::with('users')->withCount('users')->get();
Also, you can use DB::select('select * from xxx group by yyy') to execute the query.
In my example code I am using the php client library, but it should be understood by anyone familiar with elasticsearch.
I'm using elasticsearch to create an index where each document contains an array of nGram indexed authors. Initially, the document will have a single author, but as time progresses, more authors will be appended to the array. Ideally, a search could be executed by an author's name, and if any of the authors in the array get matched, the document will be found.
I have been trying to use the documentation here for appending to the array and here for using the array type - but I have not had success getting this working.
First, I want to create an index for documents, with a title, array of authors, and an array of comments.
$client = new Client();
$params = [
'index' => 'document',
'body' => [
'settings' => [
// Simple settings for now, single shard
'number_of_shards' => 1,
'number_of_replicas' => 0,
'analysis' => [
'filter' => [
'shingle' => [
'type' => 'shingle'
]
],
'analyzer' => [
'my_ngram_analyzer' => [
'tokenizer' => 'my_ngram_tokenizer',
'filter' => 'lowercase',
]
],
// Allow searching for partial names with nGram
'tokenizer' => [
'my_ngram_tokenizer' => [
'type' => 'nGram',
'min_gram' => 1,
'max_gram' => 15,
'token_chars' => ['letter', 'digit']
]
]
]
],
'mappings' => [
'_default_' => [
'properties' => [
'document_id' => [
'type' => 'string',
'index' => 'not_analyzed',
],
// The name, email, or other info related to the person
'title' => [
'type' => 'string',
'analyzer' => 'my_ngram_analyzer',
'term_vector' => 'yes',
'copy_to' => 'combined'
],
'authors' => [
'type' => 'list',
'analyzer' => 'my_ngram_analyzer',
'term_vector' => 'yes',
'copy_to' => 'combined'
],
'comments' => [
'type' => 'list',
'analyzer' => 'my_ngram_analyzer',
'term_vector' => 'yes',
'copy_to' => 'combined'
],
]
],
]
]
];
// Create index `person` with ngram indexing
$client->indices()->create($params);
Off the get go, I can't even create the index due to this error:
{"error":"MapperParsingException[mapping [_default_]]; nested: MapperParsingException[No handler for type [list] declared on field [authors]]; ","status":400}
HAD this gone successfully though, I would plan to create an index, starting with empty arrays for authors and title, something like this:
$client = new Client();
$params = array();
$params['body'] = array('document_id' => 'id_here', 'title' => 'my_title', 'authors' => [], 'comments' => []);
$params['index'] = 'document';
$params['type'] = 'example_type';
$params['id'] = 'id_here';
$ret = $client->index($params);
return $ret;
This seems like it should work if I had the desired index to add this structure of information to, but what concerns me would be appending something to the array using update. For example,
$client = new Client();
$params = array();
//$params['body'] = array('person_id' => $person_id, 'emails' => [$email]);
$params['index'] = 'document';
$params['type'] = 'example_type';
$params['id'] = 'id_here';
$params['script'] = 'NO IDEA WHAT THIS SCRIPT SHOULD BE TO APPEND TO THE ARRAY';
$ret = $client->update($params);
return $ret;
}
I am not sure how I would go about actually appending a thing to the array and making sure it's indexed.
Finally, another thing that confuses me is how I could search based on any author in the array. Ideally I could do something like this:
But I'm not 100% whether it will work. Maybe there is something fundemental about elasticsearch that I am not understanding. I am completely new to so any resources that will get me to a point where these little details don't hang me up would be appreciated.
Also, any direct advice on how to use elasticsearch to solve these problems would be appreciated.
Sorry for the big wall of text, to recap, I am looking for advice on how to
Create an index that supports nGram analysis on all elements of an array
Updating that index to append to the array
Searching for the now-updated index.
Thanks for any help
EDIT: thanks to #astax, I am now able to create the index and append to the value as a string. HOWEVER, there are two problems with this:
the array is stored as a string value, so a script like
$params['script'] = 'ctx._source.authors += [\'hello\']';
actually appends a STRING with [] rather than an array containing a value.
the value inputted does not appear to be ngram analyzed, so a search like this:
$client = new Client();
$searchParams['index'] = 'document';
$searchParams['type'] = 'example_type';
$searchParams['body']['query']['match']['_all'] = 'hello';
$queryResponse = $client->search($searchParams);
print_r($queryResponse); // SUCCESS
will find the new value but a search like this:
$client = new Client();
$searchParams['index'] = 'document';
$searchParams['type'] = 'example_type';
$searchParams['body']['query']['match']['_all'] = 'hel';
$queryResponse = $client->search($searchParams);
print_r($queryResponse); // NO RESULTS
does not
There is no type "list" in elasticsearch. But you can use "string" field type and store array of values.
....
'comments' => [
'type' => 'string',
'analyzer' => 'my_ngram_analyzer',
'term_vector' => 'yes',
'copy_to' => 'combined'
],
....
And index a document this way:
....
$params['body'] = array(
'document_id' => 'id_here',
'title' => 'my_title',
'authors' => [],
'comments' => ['comment1', 'comment2']);
....
As for the script for apending an element to array, this answer may help you - Elasticsearch upserting and appending to array
However, do you really need to update the document? It might be easier to just reindex it as this is exactly what Elasticsearch does internally. It reads the "_source" property, does the required modification and reindexes it. BTW, this means that "_source" must be enabled and all properties of the document should be included into it.
You also may consider storing comments and authors (as I understand these are authors of comments, not the document authors) as child document in ES and using "has_child" filter.
I can't really give you specific solution, but strongly recommend installing Marvel plugin for ElasticSearch and use its "sense" tool to check how your overall process works step by step.
So check if your tokenizer is properly configured by running tests as described at http://www.elastic.co/guide/en/elasticsearch/reference/1.4/indices-analyze.html.
Then check if your update script is doing what you expect by retrieving the document by running GET /document/example_type/some_existing_id
The authors and comments should be arrays, but not strings.
Finally perform the search:
GET /document/_search
{
'query' : {
'match': { '_all': 'hel' }
}
}
If you're building the query yourself rather than getting it from the user, you may use query_string with placeholders:
GET /document/_search
{
'query' : {
'query_string': {
'fields': '_all',
'query': 'hel*'
}
}
}
Laravel 4.1. I want to update a city, check the rules and it fails on unique check.
Rules:
public static $rules = [
'name' => 'required|alpha_dash|unique:cities',
'slug' => 'alpha_dash|unique:cities',
'seo_title' => 'required|max:60|unique:cities',
'seo_description' => 'required|max:160|unique:cities',
'rank' => 'integer',
'visible' => 'integer'
];
I know, I can smth like:
'name' => 'required|alpha_dash|unique:cities, name, ##',
where ## - id, but I cant dynamically set id to updated one.
'name' => "required|alpha_dash|unique:cities, name, $id", // doesnt work
'name' => "required|alpha_dash|unique:cities, name, $this->id", // doesnt work
Is there any way to do it normally ?
You can do it in separate ways.
An easy way is to use different rules based on different actions . When you will create the model, you will use the rules that you currently have.
When you will update the model, you will change the unique:cities to exists:cities
I usually do this with a validation service.
You create a base abstract Validator in services/ , which has a passes() function.
For each model, you create a ModelValidator , in your case CityValidator. Where you put your rules like :
public static $rules = [
'new'=>[
'name' => 'required|alpha_dash|unique:cities',
'slug' => 'alpha_dash|unique:cities',
'seo_title' => 'required|max:60|unique:cities',
'seo_description' => 'required|max:160|unique:cities',
'rank' => 'integer',
'visible' => 'integer'],
'edit'=>[
'name' => 'required|alpha_dash|exists:cities',
'slug' => 'alpha_dash|unique:cities',
'seo_title' => 'required|max:60|exists:cities',
'seo_description' => 'required|max:160|exists:cities',
'rank' => 'integer',
'visible' => 'integer'
]
];
The 3rd argument accepts a value to be ignored... If you want to do a WHERE clause, do it like:
'name' => array("required", "alpha_dash", "unique:cities,name,null,id,id,$this->id"...
The docs says:
Adding Additional Where Clauses
You may also specify more conditions that will be added as "where"
clauses to the query:
'email' => 'unique:users,email_address,NULL,id,account_id,1'
In the rule above, only rows with an account_id of 1 would be included in the unique check.
Learn by example:
email => unique:users,email_address,id,NULL,field_1,value_1,field_2,value_2,field_x,value_x
Generates the query:
SELECT
count(*) AS AGGREGATE
FROM
`entries`
WHERE
`email_address` = ?
AND `id` <> NULL
AND `field_1` = `value_1`
AND `field_2` = `value_2`
AND `field_x` = `value_x`
I found an elegant-ish way to do this using fadion/ValidatorAssistant:
<?php
use Fadion\ValidatorAssistant\ValidatorAssistant;
class CityValidator extends ValidatorAssistant {
// standard rules
public static $rules = [
'name' => 'required|alpha_dash',
'slug' => 'alpha_dash',
'seo_title' => 'required|max:60',
'seo_description' => 'required|max:160',
];
// some preparation before validation
protected function before()
{
// Inject the given city id into the unique rules
$this->rules['name'] .= 'unique:cities,name,' . $this->inputs['id'];
$this->rules['slug'] .= 'unique:cities,slug,' . $this->inputs['id'];
$this->rules['seo_title'] .= 'unique:cities,seo_title,' . $this->inputs['id'];
$this->rules['seo_description'] .= 'unique:cities,seo_description,' . $this->inputs['id'];
}
There's almost certainly a more elegant way to do this when you need several fields to be unique database-wide, but the above works very well for times when you only need one part to be unique.
I have been fighting with this code:
function getNextActionFObyBalance($when) {
$theQuery = $this->find('first', array(
'fields' => array(
'Contract.id',
'Contract.start_balance'
),
'conditions' => array(
'AND' => array(
'Status.next_action_by' => 'frontoffice',
'Status.status_type' => 'active',
'Status.visibility' => 'frontoffice',
'OR' => array(
'Contract.next_action_on' => null,
'Contract.next_action_on <=' => $when
)
)),
'order' => 'Contract.start_balance DESC',
'recursive' => 0,
));
return $theQuery;
}
I have enabled logging on the MySQL server at this is what the server indicates that CakePHP is requesting:
SELECT `Contract`.`id`, `Contract`.`start_balance` FROM `contracts` AS `Contract` LEFT JOIN `statuses` AS `Status` ON (`Contract`.`status_id` = `Status`.`id`) LEFT JOIN `users` AS `User` ON (`Contract`.`user_id` = `User`.`id`) WHERE ((`Status`.`next_action_by` = 'frontoffice') AND (`Status`.`status_type` = 'active') AND (`Status`.`visibility` = 'frontoffice') AND (((`Contract`.`next_action_on` IS NULL) OR (`Contract`.`next_action_on` <= '2010-09-13 10:13:04')))) ORDER BY `Contract`.`start_balance` DESC LIMIT 1
if I use that in the phpmyadmin tool, I get exactly what I was expecting 1 record with two fields. BUT CakePHP just gives me an empty result set.
Can anyone enlighten me?
PS the code was working but I can figure out what changed!
The problem was with a stub to do some post processing afterFind. The problem is that I have completely forgotten to return $results;
I found the error by doing a step by step debugging down the find method in model.php. Found that the after find was called at some point and went to check my afterFind.
Took my about 4 hours for a simple error but I am learning!
Presumably this method is defined in models/contract.php?
The recursive = 0 statement looks a bit suspect to me. Are the models correctly related in their respective model files?
Have you tried loadModel in case the associations aren't working properly?
It would be useful to see the relationship definitions from the respective models.
--EDIT--
I've formatted the code from your comment here as I can't edit your OP
var $belongsTo = array(
'Status' => array(
'className' => 'Status',
'foreignKey' => 'status_id',
),
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
)
);
var $hasMany = array(
'Transaction' => array(
'className' => 'Transaction',
'foreignKey' => 'contract_id',
'dependent' => false,
)
);