My application allows a user to create scenarios by linking together soe_blocks. In turn, soe_blocks refer to a variable number of soe_entries.
To build scenarios, soe_blocks are linked to the scenario and ordered by an offset. The soe_blocks can be used in many different scenarios. soe_entries can relate only to a single soe_block
I think the relationship is defined as:
scenarios belongsToMany soe_blocks through scenarios_soe_blocks
soe_blocks belongsToMany scenarios through scenarios_soe_blocks
scenarios_soe_blocks is where the offset is kept
soe_entries haveOne soe_blocks
Tables:
scenarios: id | name
data: 0, 'scenario_1'
soe_blocks: id | name
data: 0, 'soe_block_1'
1, 'soe_block_2'
scenarios_soe_blocks: id | scenario_id | soe_block_id | offset
data: 1, 0, 1, 1
2, 0, 2, 2
Models:
class ScenariosTable extends Table
{
$this->belongsToMany('SoeBlocks', [
'foreignKey' => 'scenario_id',
'targetForeignKey' => 'soe_block_id',
'through' => 'ScenariosSoeBlocks',
'joinTable' => 'soe_blocks'
]);
}
class SoeBlocksTable extends Table
{
$this->belongsToMany('Scenarios', [
'foreignKey' => 'soe_block_id',
'targetForeignKey' => 'scenario_id',
'joinTable' => 'scenarios_soe_blocks',
'through' => 'ScenariosSoeBlocks'
]);
}
class ScenariosSoeBlocksTable extends Table
$this->belongsTo('SoeBlocks', [
'foreignKey' => 'soe_block_id',
'joinType' => 'INNER'
]);
}
Controllers:
public function view($id = null)
{
$scenario = $this->Scenarios->get($id, [
'contain' => ['SoeBlocks', 'RunStatus', 'ScenarioLog']
]);
$this->set('scenario', $scenario);
}
As far as I can make out from CakePHP Doc, this is all I need. But I couldn't get the ScenarioController->view() method to return the offsets from the scenarios_soe_blocks table associated with the soe_blocks.
I tried to add ScenariosSoeBlocks into the 'contain' clause in the ScenarioController, but got the error: Scenarios is not associated with ScenariosSoeBlocks. I found an SO article that suggested I add the following to the ScenarioTable:
$this->hasMany('ScenariosSoeBlocks', [
'foreignKey' => 'scenario_id'
]);
This seems to have worked, and now I can request ScenariosSoeBlocks in my controller like this:
$scenario = $this->Scenarios->get($id, [
'contain' => ['SoeBlocks', 'ScenariosSoeBlocks', 'RunStatus', 'ScenarioLog']
]);
Which at least gets the data into the view template, but not in the single object I'm hoping for. Eventually, I want to be able to CRUD the soe_blocks along with their associated soe_entries, in an object that looks like this:
offset | soe_block_id | soe_entry_id |
I have many other questions, like how to save etc., but I figured I need to get this working first.
So, my questions for now are:
are my associations correct?
how do I retrieve all the associations to view?
are my associations correct?
The first two are, but then it should be:
soe_blocks hasOne soe_entries
soe_entries belongsTo soe_blocks
how do I retrieve all the associations to view?
By containing them, just like you did in your first example. This question seems to originate from the question how to access the join table data, which is very simple, the join table data is being set on the target table entity (Scenario or SoeBlock, depending on from which side/table you issue the query), in a property named _joinData:
$joinTableEntity = $scenario->soe_blocks[0]->_joinData;
$offset = $joinTableEntity->offset;
You can easily gather information about the data structure by dumping your entity contents:
debug($scenario);
See also
Cookbook > Database Access & ORM > Associations - Linking Tables Together
Cookbook > Database Access & ORM > Saving Data > Saving Additional Data to the Join Table
Related
Application is using CakePHP version 3.5.18
I have used the ORM to obtain a list which returns key/value pairs:
$Displays = TableRegistry::get('Displays');
$displays = $Displays->find('list', ['keyField' => 'id', 'valueField' => 'id'])->where(...)->toArray();
This works as expected. An example of debug($displays); looks like this:
(int) 36 => (int) 36,
(int) 160 => (int) 160,
(int) 149 => (int) 149,
...
In this particular application there are 3 Models which work in the following hierarchy:
Regulations (Level 1. Table name regulations)
Groups (Level 2. Table name groups)
Displays (Level 3. Table name displays)
The application has been baked so that the appropriate relationships are defined in the Table classes.
// src/Model/Table/RegulationsTable.php
$this->hasMany('Groups', [
'foreignKey' => 'regulation_id'
]);
// src/Model/Table/GroupsTable.php
$this->hasMany('Displays', [
'foreignKey' => 'group_id'
]);
// src/Model/Table/DisplaysTable.php
$this->belongsTo('Groups', [
'foreignKey' => 'group_id',
'joinType' => 'INNER'
]);
What I want to do is adjust the query defined by $displays so that the results correspond to a given array of Regulations.id. The vanilla SQL equivalent of what I'm trying to do is ... WHERE regulations.id IN (1,2,3) assuming the ID's were 1, 2 and 3.
When I read the Cake docs on Passing conditions to contain it says:
When you limit the fields that are fetched from an association, you must ensure that the foreign key columns are selected. Failing to select foreign key fields will cause associated data to not be present in the final result.
I don't understand how this is possible with find('list') because that only selects the keyField and valueField specified by the developer? In this case that would be displays.id for both.
In any case I don't understand how to write this query because in the same linked documentation it says
When using contain() you are able to restrict the data returned by the associations and filter them by conditions. To specify conditions, pass an anonymous function that receives as the first argument a query object, \Cake\ORM\Query
What would the contain() actually operate on: I assume it's Regulations but don't know how to write this.
I think you need to look at filtering data by matching, not through contains. (If I understand correctly.)
You can combine find('list') with ->matching(...):
$displays = $this->Displays->find('list')
->matching('Groups.Regulations', function ($q) use ($ids) {
return $q->where(['Regulations.id IN' => $ids]);
})
->toArray();
(Still untested)
Filtering by associated data
I would like to create a junction table tbl_guid_cost_centre that gets taken care of without me manually saving it to the database. I tried adding this to my relations:
'costCentre' => [
self::HAS_ONE,
'CostCentre',
'guid_to',
'foreignKey' => 'guid',
'tbl_guid_cost_centre(guid_to, cost_center_id)',
"order" => "id desc"],
so that my when saving the costCentre, a row is created for it in my tbl_guid_cost_centre. However I'm getting the error:
Property "CHasOneRelation.0" is not defined.
Any suggestion?
You can have your junction table with the keyword through in your relations:
public function relations() {
'guidCostCentre' => [
self::HAS_ONE,
'GuidCostCentre',
['guid_to' => 'guid']
],
'costCentre' => [
self::HAS_ONE,
'CostCentre',
'cost_centre_id',
'through' => 'guidCostCentre'
]
}
You're defining HAS_ONE relation in a wrong way. The first three elements of relation configuration array should be: relation type, related model name and foreign keys definition. All further elements should be indexed by keys related to relation properties. 'tbl_guid_cost_centre(guid_to, cost_center_id)', probably generates this error, because it does not have a key, so it is treaded as a value for 0 property. You didn't share any details, so it is hard to guess what you want to achieve, but you should start from something like this:
'costCentre' => [
self::HAS_ONE,
'CostCentre',
'guid_to',
'order' => 'id desc',
],
And add additional settings at the end array with correct key.
Some examples and explanation you can find in the documentation: https://www.yiiframework.com/doc/guide/1.1/en/database.arr#declaring-relationship
i'd like to set hasOne and hasMany to same model,in a part of my code i need only 1 result, but in other part i need all result (Objects from type Client that will return for a table in my site):
$this->hasOne('Vendas')
->setForeignKey('id_cliente')
->setBindingKey('id')
;
$this->hasMany('Vendas')
->setForeignKey('id_cliente')
->setBidingKey('id');
This is possible, or i made a mistake?
Read the manual https://book.cakephp.org/3.0/en/orm/associations.html. Read the whole page carefully.
className: the class name of the table being associated to the current model. If you’re defining a ‘User hasOne Address’ relationship, the className key should equal ‘Addresses’.
conditions: an array of find() compatible conditions such as ['Addresses.primary' => true]
Define the class name and conditions you need for your assocs.
$this->hasOne('Foo', [
'className' => 'Foo',
'conditions' => [/* whatever you need*/]
]);
$this->hasMany('Bar', [
'className' => 'Foo',
'conditions' => [/* whatever you need*/]
]);
If the relationship is 1-* you should define the relationship as hasMany(). Then you write a query for one result and a query for multiple results
I have defined the following associations:
class RecipesTable extends Table
{
$this->belongsToMany('Ingredients', [
'through' => 'RecipesIngredients',
'foreignKey' => 'recipe_id',
'targetForeignKey' => 'ingredient_id',
]);
class IngredientsTable extends Table
{
$this->belongsToMany('Recipes', [
'through' => 'RecipesIngredients',
'foreignKey' => 'ingredient_id',
'targetForeignKey' => 'recipe_id',
]);
class RecipesIngredientsTable extends Table
{
$this->belongsTo('Recipes');
$this->belongsTo('Ingredients');
$this->belongsTo('Units');
The table 'RecipesIngredients' has the following structure:
id | recipe_id | ingredient_id | unit_id | ...
Now I make a request like the one below to get Recipes and the associated Ingredients. But without the Units.
$data = $this->Recipe->find('all')
->where('Recipe.id' => 55)
->contain(['Ingredient', ...])
->all();
My question is: how do I get the data of the associated 'Units' in a call of $this->Recipe?
I tried different contains like ->contain(['Ingredient' => ['Unit'], ...]) (and so on) but this doesn't work. CakePHP just returns the associated ingredients and the contents of the 'through' join table without linking to the associated units. Or gives an error of missing associations.
That won't work using contain(), at least not with a belongsToMany association, as the on-the-fly created intermediate association for the join table is being created too late for the eager loader to recognize it.
What you can do is explicitly create the otherwise on-the-fly generated hasMany association for the join table manually, eg on the RecipesTable class add:
$this->hasMany('RecipesIngredients', [
'foreignKey' => 'recipe_id'
]);
Then you can contain your associations like:
->contain(['RecipesIngredients' => ['Ingredients', 'Units']])
i have the following two tables:
recipes
similiar_recipes
As you can see similar_recipes has two foreign keys which both point to recipes. Now I want two things. First of the linking. I read on stackoverflow some similar stuff and come up with the following configuration:
RecipesTable.php
$this->hasMany('Recipes', [
'foreignKey' => 'recipe_id',
'className' => 'SimilarRecipes'
]);
$this->hasMany('SimilarRecipes', [
'foreignKey' => 'similar_recipe_id',
'className' => 'SimilarRecipes'
]);
SimilarRecipesTable.php
$this->belongsTo('Recipes', [
'foreignKey' => 'recipe_id',
'joinType' => 'INNER',
'className' => 'Recipes'
]);
$this->belongsTo('SimilarRecipes', [
'foreignKey' => 'similar_recipe_id',
'joinType' => 'INNER',
'className' => 'Recipes'
]);
The configuration should be correct. Now the other question is the correct associated saving or lets ask better is it possible to do the following:
Recipes Data
Now in Cake I want to add a recipe and the associated recipes which are delivered in the request->data as an id-array
$newRecipe = $this->Recipes->newEntity();
$newRecipe = $this->Recipes->patchEntity($newRecipe, $this->request->data);
$this->Recipes->save($newRecipe, ['associated' => ['SimilarRecipes']])
This should be the result:
In conclusion I saved a new recipe which gets the id 3. In the request->data I select the similar recipes 1 and 2.
Could someone give me an advice. Is my configuration wrong. Or what do I have to pass to the save method? By the way I don't get any errors.
Use belongsToMany associations instead
I'd say your association approach is wrong (or at least makes things unnecessarily complicated), I'd suggest to use belongsToMany associations instead, as what you seem to create there is a self-joining many-to-many relation.
Name the table recipes_similar_recipes, that's the convention CakePHP uses, it helps to avoid association name conflicts/confusion, and allows relying on magic configuration. Your tables associations/configuration should then look something like:
RecipesTable.php
$this->belongsToMany('SimilarRecipes', [
'foreignKey' => 'recipe_id',
'targetForeignKey' => 'similar_recipe_id',
'joinTable' => 'recipes_similar_recipes'
]);
SimilarRecipesTable.php
$this->table('recipes');
$this->primaryKey('id');
$this->belongsToMany('Recipes', [
'foreignKey' => 'similar_recipe_id',
'targetForeignKey' => 'recipe_id',
'joinTable' => 'recipes_similar_recipes'
]);
With such a setup you could then use the array of IDs variant, eg use the _ids key to define the existing (similar) recipies that should be associated with your new recipe, ie the request data should look something like:
[
// there is no title column in your example,
// this should just clarify the data structure
'title' => 'New recipe that will get the ID 3',
'similar_recipes' => [
'_ids' => [
1, 2
]
]
]
which should populate the recipes_similar_recipes table like:
+----+-----------+-------------------+
| id | recipe_id | similar_recipe_id |
+----+-----------+-------------------+
| 1 | 3 | 1 |
| 2 | 3 | 2 |
+----+-----------+-------------------+
You should then also consider making recipe_id and similar_recipe_id a compound primary key, or at least create a unique index with them.
See also
Cookbook > Database Access & ORM > Associations - Linking Tables Together > BelongsToMany Associations
Cookbook > Database Access & ORM > Saving Data > Converting BelongsToMany Data