Edited at

cakePHP3でbelongsToMany

More than 3 years have passed since last update.

belongsToMany、とくにjoinTableにデータを持たせたいときに、どうやってデータを保存して、取り出せば良いのか・・・色々調べたのでメモします。


テーブル・モデル作成

phinxでテーブルを作っておきます。

今回は、UserとDogの関係をhasMany、DogとDogChildrenの関係をbelongsToManyとしています。

public function change()

{
$this->table('users')
->addColumn('name', 'string', array('limit' => 20))
->create();

$this->table('dogs')
->addColumn('name', 'string', array('limit' => 20))
->addColumn('user_id', 'integer')
->addForeignKey('user_id', 'users', 'id')
->create();

$this->table('dog_children')
->addColumn('name', 'string', array('limit' => 20))
->create();

$this->table('dogs_dog_children')
->addColumn('dog_id', 'integer')
->addColumn('dog_child_id', 'integer')
->addColumn('flg', 'boolean')
->create();
}

マイグレーションできたら、モデルも作ります。

./bin/cake migrations migrate

./bin/cake bake model users
./bin/cake bake model dogs
./bin/cake bake model dog_children
./bin/cake bake model dogs_dog_children


データ保存

データの保存も取得も、なんと一気にできます(@AnzNetJpさん、ありがとうございます。)。

$users = TableRegistry::get('users');

$userArray = [
'name' => 'Tony',
'dogs' => [[
'name' => 'pochi',
'dog_children' => [[
'name' => 'tama',
'_joinData' => ['flg' => true]
]]
]]];
$users->save($users->newEntity($userArray, ['associated' => 'Dogs.DogChildren._joinData']));

ポイントは、_joinDataを使ってjoinTableにデータを保存しているところです。

あと、dog_childrenのvalueが二重配列になってるのも間違えやすいところだと思います。

newEntityを作るときのassociatedOptionは必須です。

ただ、dog_childrenのnameがユニークの必要がある場合、下記のようなデータは保存できません。

理由は、tamaという名前を持つDogChildEntityが毎回saveされるためです。

$userArray = [

'name' => 'Tony',
'dogs' => [[
'name' => 'pochi',
'dog_children' => [[
'name' => 'tama',
'_joinData' => ['flg' => true]
],
[
'name' => 'tama',
'_joinData' => ['flg' => false]
]]
]]];

こういうときは、あらかじめtamaという名前のDogChildEntityを保存しておき、プロパティに_joinDataを追加すると大丈夫です。

$dogChildren = TableRegistry::get('dog_children');

$dogChild = $dogChildren->newEntity();
$dogChild->name = 'tama';
$dogChildren->save($dogChild);

$dogChild_1 = $dogChildren->find()->where(['name' => 'tama'])->first();
$dogChild_1->_joinData->flg = true;

$dogChild_2 = $dogChildren->find()->where(['name' => 'tama'])->first();
$dogChild_2->_joinData->flg = false;

$userArray = [
'name' => 'Tony',
'dogs' => [[
'name' => 'pochi',
'dog_children' => [$dogChild_1, $dogChild_2]
]]];


データ取得

では、保存されたデータを見てみましょう。

$users = TableRegistry::get('users');

$user = $users->find()->contain(['Dogs', 'Dogs.DogChildren'])->first();

これで$userには、Dogs、Dogs.DogChildrenのデータを持ったUserEntityが代入されます。

cakePHP3からrecursiveはなくなってしまったので、代わりにcontainを使って関連データを取得できます。

ちなみに、find()のほかにget()もあり、引数にprimary_keyを入れることでデータを取ってこれるのですが、これはget()を動かした時点でEntityが返ってくるので、メソッドチェーンでcontainを使うことができません。

dogsやdog_childrenは配列に入っているので、こんな感じで取ってこれます。

$user = $users->find()->contain(['Dogs', 'Dogs.DogChildren'])->first();

foreach ($user->dogs as $dog) {
var_dump($dog);
foreach ($dog->dog_children as $dog_child) {
var_dump($dog_child);
var_dump($dog_child->_joinData);
}
}
/*
class App\Model\Entity\Dog#121 (10) {
protected $_accessible =>
array(4) {
'name' =>
bool(true)
'user_id' =>
bool(true)
'user' =>
bool(true)
'dog_children' =>
bool(true)
}
protected $_properties =>
array(4) {
'id' =>
int(1)
'name' =>
string(5) "pochi"
'user_id' =>
int(1)
'dog_children' =>
array(1) {
[0] =>
class App\Model\Entity\DogChild#114 (10) {
...
}
}
}
protected $_original =>
array(0) {
}
protected $_hidden =>
array(0) {
}
protected $_virtual =>
array(0) {
}
protected $_className =>
string(20) "App\Model\Entity\Dog"
protected $_dirty =>
array(0) {
}
protected $_new =>
bool(false)
protected $_errors =>
array(0) {
}
protected $_registryAlias =>
string(4) "Dogs"
}
class App\Model\Entity\DogChild#114 (10) {
protected $_accessible =>
array(2) {
'name' =>
bool(true)
'dogs' =>
bool(true)
}
protected $_properties =>
array(3) {
'id' =>
int(1)
'name' =>
string(4) "tama"
'_joinData' =>
class App\Model\Entity\DogsDogChild#111 (10) {
protected $_accessible =>
array(5) {
...
}
protected $_properties =>
array(4) {
...
}
protected $_original =>
array(0) {
...
}
protected $_hidden =>
array(0) {
...
}
protected $_virtual =>
array(0) {
...
}
protected $_className =>
string(29) "App\Model\Entity\DogsDogChild"
protected $_dirty =>
array(0) {
...
}
protected $_new =>
bool(false)
protected $_errors =>
array(0) {
...
}
protected $_registryAlias =>
string(15) "DogsDogChildren"
}
}
protected $_original =>
array(0) {
}
protected $_hidden =>
array(0) {
}
protected $_virtual =>
array(0) {
}
protected $_className =>
string(25) "App\Model\Entity\DogChild"
protected $_dirty =>
array(0) {
}
protected $_new =>
bool(false)
protected $_errors =>
array(0) {
}
protected $_registryAlias =>
string(11) "DogChildren"
}
class App\Model\Entity\DogsDogChild#111 (10) {
protected $_accessible =>
array(5) {
'dog_id' =>
bool(true)
'dog_child_id' =>
bool(true)
'flg' =>
bool(true)
'dog' =>
bool(true)
'dog_child' =>
bool(true)
}
protected $_properties =>
array(4) {
'dog_child_id' =>
int(1)
'id' =>
int(1)
'dog_id' =>
int(1)
'flg' =>
bool(true)
}
protected $_original =>
array(0) {
}
protected $_hidden =>
array(0) {
}
protected $_virtual =>
array(0) {
}
protected $_className =>
string(29) "App\Model\Entity\DogsDogChild"
protected $_dirty =>
array(0) {
}
protected $_new =>
bool(false)
protected $_errors =>
array(0) {
}
protected $_registryAlias =>
string(15) "DogsDogChildren"
}
*/

さて、dogs_dog_childrenに作ったflgはどこにあるかというと・・・dog_childの_joinDataの中にありましたー!

(すみません、Ctrl+Fで検索でもしてみてください。。)

まぁ、保存するときに_joinData使いましたし、当然といえば当然なのですが、私はデータの保存ではなく取得から作り始めたため、どうやって取得すればいいのか結構迷いました・・・。

では、良いcakeLifeを!!\(^o^)/