ปกติการสร้างฟอร์มเพื่อบันทึกและแก้ไขข้อมูลนั้น เราจะทำเพียงแค่ครั้งละ 1 Model แต่ก็มีในบางครั้งที่เราจำเป็นจะต้องบันทึกพร้อมกันทีละหลายๆ model ซึ่งใน Yii 2 นั้นสามารถทำได้ง่ายๆ เลย จะทำกี่ model ก็ได้ ส่วนใหญ่จะเป็นตารางที่มีการเชื่อมกัน (Relation) อยู่แล้ว
โครงสร้างและหลักการทำงาน
การทำงานจะเหมือนกันทั้งสองแบบ ต่างกันแค่เรื่องของการ validate ข้อมูลเท่านั้น ซึ่งปกติจะใช้ฟังก์ชัน validate() ผ่าน model แต่ถ้าหากต้องบันทึกพร้อมกันทีละหลายๆ model จะต้องใช้ validateMultiple() แทน ลองดูความแตกต่างตามภาพ
Single model
ในการบันทึกข้อมูลปกติจะมีอยู่ 4 ขั้นตอนคือ
$model = new SomeModel()
แล้วนำไปแสดงผลเป็นฟอร์มเพื่อรับข้อมูล- เมื่อมีการ submit จะรับข้อมูลจากฟอร์มโดยใช้ฟังก์ชัน
$model->load()
- ตรวจสอบข้อมูลที่โหลดเข้ามาโดยใช้
$model->validate()
- นำข้อมูลที่ได้ไปบันทึก
Multiple model
เมื่อลองเปรียบเทียบกันดูจะเห็นความแตกต่าง 2 จุดคือ ปกติจะใช้ฟังก์ชัน validat() ใน model แต่ถ้าหากเป็นทีละหลายๆ model จะใช้ Model::validateMultiple() แทน ซึ่ง validate() แบบเดิมจะไม่สามารถ validate ได้ทีละหลายๆ model ในการบันทึกข้อมูลจะมีอยู่ 4 ขั้นตอนคือ
$model = new ModelA(), $model = new ModelB()
แล้วนำไปแสดงผลเป็นฟอร์มเพื่อรับข้อมูล- เมื่อมีการ submit จะรับข้อมูลจากฟอร์มโดยใช้ฟังก์ชัน
$modelA->load(),$modelB->load()
แยกกัน - ตรวจสอบข้อมูลที่โหลดเข้ามาโดยใช้
Model::validateMultiple()
ร่วมกันทั้ง 2 model - นำข้อมูลที่ได้ไปบันทึก
ปกติในการใช้งานเราจะไม่เห็นการเรียกใช้งานฟังก์ชัน validate() เพราะมันถูกเรียกใช้งานอยู่ภายในฟังก์ชัน save() อยู่แล้ว
ลงมือสร้างฟอร์ม
ตัวอย่างนี้เราจะลองสร้างตาราง 2 ตารางแบบง่ายๆ โดยตารางแรกชื่อ m_user เก็บข้อมูล username,email ตารางที่ 2 m_emp เก็บข้อมูล คำนำหน้า,ชื่อ,นามสกุล ซึ่งตาราง m_emp จะเก็บข้อมูล user_id เพื่อ relation กับ m_user ด้วย id และเมื่อมีการบันทึกข้อมูลเราจะให้มันสามารถบันทึกทั้ง 2 ตารางได้พร้อมกัน
ให้สร้างตารางตามโครงสร้างนี้
CREATE TABLE `m_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(150) DEFAULT NULL,
`email` varchar(150) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
CREATE TABLE `m_emp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(100) DEFAULT NULL,
`name` varchar(150) DEFAULT NULL,
`surname` varchar(150) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
จากนั้นให้สร้าง model ด้วย gii ทั้ง 2 ตาราง เป็น MUser,MEmp หลังจากสร้าง model เสร็จให้ gii สร้าง crud ตาราง m_emp ต่อเพื่อสร้างฟอร์มเพิ่มลบแก้ไข แต่ไม่ต้องสร้าง crud ของ m_user เพราะเราจะนำไปรวมที่ฟอร์ม m_emp ทดลองเข้าใช้งาน index?r=m-emp/index
ว่าสามารถใช้งานได้ทุกอย่างเพิ่ม, ลบ, แก้ไข
ปรับปรุงส่วนต่างๆ
ส่วนแรกที่ต้องปรับปรุงคือ actionCreate() ไปที่ไฟล์ controllers/MEmpController.php
ปรับปรุง actionCreate() ใน MEmpController.php
ของเดิมจะ new object ขึ้นมาแค่ตัวเดียวคือ MEmp()
<?php
// ...
public function actionCreate()
{
$model = new MEmp();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
ปรับปรุงใหม่โดย use MUser เพิ่มเติมด้านบนสุด
<?php
use backend\models\MUser;
// ...
public function actionCreate()
{
$modelEmp = new MEmp();
$modelUser = new MUser();
if($modelEmp->load(Yii::$app->request->post()) &&
$modelUser->load(Yii::$app->request->post()) &&
Model::validateMultiple([$modelEmp,$modelUser]))
{
if($modelUser->save()){
$modelEmp->user_id = $modelUser->id;
$modelEmp->save();
}
return $this->redirect(['view', 'id' => $modelEmp->id]);
} else {
return $this->render('create', [
'model' => $modelEmp,
'modelUser'=>$modelUser
]);
}
}
สังเกตว่าเราจะต้อง new object ที่ต้องการขึ้นมาจะใช้กี่ตัวในฟอร์มก็ต้อง new object ตามจำนวนที่ต้องการ จากนั้นจะส่ง model ไปแสดงผลที่ฟอร์ม
เมื่อมีการ กด submit เข้ามาจะสังเกตว่า model จะเรียกข้อมูลที่ submit เข้ามาด้วยฟังก์ชัน load() ของใครของมัน จากนั้นก็เรียก Model::validateMultiple()
เพื่อ validate ข้อมูล
เมื่อ validate เสร็จเรียบร้อย ก็นำข้อมูลที่ได้ผ่านการตรวจสอบแล้วนำไปบันทึกโดยเราจะให้ MUser บันทึกก่อนเพราะว่า MEmp ต้องใช้ user_id ซึ่งเป็น pk ของ MUser จากนั้นก็จะสามารถบันทึกได้ทั้ง 2 ตาราง
ปรับปรุง views/m-emp/_form.php
เราจะทำการเรียกฟอร์มของ user เข้าไปด้วยโดยใช้ตัวแปร $modelUser และทำการปรับปรุง layout เล็กน้อย
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model backend\models\MEmp */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="memp-form">
<?php $form = ActiveForm::begin(); ?>
<div class="panel panel-default">
<div class="panel-body">
<h3>Employee</h3>
<div class="row">
<div class="col-md-2">
<?= $form->field($model, 'title')->textInput(['maxlength' => true]) ?>
</div>
<div class="col-md-5">
<?= $form->field($model, 'name')->textInput(['maxlength' => true]) ?>
</div>
<div class="col-md-5">
<?= $form->field($model, 'surname')->textInput(['maxlength' => true]) ?>
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-body">
<h3>User</h3>
<?= $form->field($modelUser, 'username')->textInput(['maxlength' => true]) ?>
<?= $form->field($modelUser, 'email')->textInput(['maxlength' => true]) ?>
</div>
</div>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
เมื่อทดสอบเรียกไปที่หน้า index.php?r=m-emp/create
ก็จะพบหน้าตาดังนี้
ทดลอง create ดู
ปรับปรุง actionUpdate() ใน MEmpController.php
<?php
public function actionUpdate($id)
{
$model = $this->findModel($id);
$modelUser = $this->findModelUser($model->user_id);
if (
$model->load(Yii::$app->request->post()) &&
$modelUser->load(Yii::$app->request->post()) &&
Model::validateMultiple([$model,$modelUser])
) {
if($modelUser->save()){
$model->save();
}
return $this->redirect(['view', 'id' => $model->id]);
} else {
return $this->render('update', [
'model' => $model,
'modelUser'=>$modelUser
]);
}
}
protected function findModelUser($id)
{
if (($model = MUser::findOne($id)) !== null) {
return $model;
} else {
throw new NotFoundHttpException('The requested page does not exist.');
}
}
ฟังก์ชัน update จะแตกต่างจาก create เล็กน้อยตรงที่ดึงข้อมูลมาก่อนเพื่อ update ซึ่งปกติหากเรา gii crud จะมี findModel() ซึ่งเป็นของ MEmp เพราะฉะนั้นเราจึงต้องสร้าง findModelUser()
ขึ้นมาเพิ่มเติมเพื่อดึงข้อมูลส่วนของ user
ทดลอง create,update ข้อมูลดู
ในการทำ one form multiple model ส่วนที่สำคัญที่สุดจะมีเพียงส่วน Model::multipleValidate() เท่านั้น
แก้ไข models/MEmp.php
เพิ่มการ validate required เข้าไป และสร้าง relation เชื่อกับ model MUser เพิ่มคำอธิบายฟิวด์ในส่วน attributeLabels()
<?php
// ...
public function rules()
{
return [
[['title','name','surname'], 'required'],
[['user_id'], 'integer'],
[['title'], 'string', 'max' => 100],
[['name', 'surname'], 'string', 'max' => 150]
];
}
public function attributeLabels()
{
return [
'id' => 'ID',
'title' => 'Title',
'name' => 'Name',
'surname' => 'Surname',
'user_id' => 'User ID',
'username' => 'รหัสผู้ใช้งาน',
'fullname' => 'ชื่อ-นามสกุล',
];
}
// ...
public function getUser(){
return $this->hasOne(MUser::className(),['id'=>'user_id']);
}
public function getFullname(){
return $this->title.$this->name.' '.$this->surname;
}
public function getUsername(){
return $this->user->username;
}
แต่ละตัวมีหน้าที่ดังนี้
getUser()
เป็นฟังก์ชันที่เอาไว้เชื่อมกับตาราง m_usergetFullname()
แสดงคำนำหน้าชื่อ, ชื่อ, นามสกุลgetUsername()
แสดงค่า username ของตาราง m_user ซึ่งเรียกผ่านตัว relation ที่ชื่อgetUser
ตัวอย่างการเรียก ฟังก์ชัน เช่น$model->fullnam
โดยเราเตรียมฟังก์ชันเหล่านี้ไว้เพื่อใช้ในตอนแสดงผลที่ gridview หรือ DetailView หรือที่อื่นๆ ก็ได้
แก้ไข models/MUser.php
เพิ่มเฉพาะในส่วนฟังก์ชัน rules()
<?php
// ...
public function rules()
{
return [
[['username'], 'required'],
[['id'], 'integer'],
[['username', 'email'], 'string', 'max' => 150],
['email','email']
];
}
เพิ่มการค้นหาและจัดเรียงข้อมูล ( Filter & Sort )
เราจะเพิ่มเติมส่วนค้นหาและการจัดเรียงข้อมูลในส่วนของหัวคอลัมน์
แก้ไข models/MEmpSearch.php
ประกาศตัวแปรเพิ่มเติม เพื่อให้สามารถเรียกใช้งานใน model ได้เพราะ 2 ฟิวด์นี้ไม่มีอยู่จริงใน table m_emp
public $fullname;
public $username;
และอย่าลืมกำหนด validate ใน rule เพื่อให้ model สามารถใช้งาน 2 ฟิวด์นี้ได้
public function rules()
{
return [
[['id', 'user_id'], 'integer'],
[['title', 'name', 'surname','fullname','username'], 'safe'],
];
}
เราจะเพิ่มส่วนการจัดเรียงข้อมูลเข้าไป มี fullname,username ดังนี้
$dataProvider->sort->attributes['fullname']=[
'asc'=>['name'=>SORT_ASC,'surname'=>SORT_DESC],
'desc'=>['name'=>SORT_DESC,'surname'=>SORT_DESC],
];
$dataProvider->sort->attributes['username']=[
'asc'=>['m_user.username'=>SORT_ASC],
'desc'=>['m_user.username'=>SORT_DESC],
];
ส่วนการค้นหาเพิ่ม fullname
$query->andWhere('name like "%'.$this->fullname.'%" or surname like "%'.$this->fullname.'%" ');
โค้ดทั้งหมด
<?php
namespace backend\models;
use Yii;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use backend\models\MEmp;
/**
* MEmpSearch represents the model behind the search form about `backend\models\MEmp`.
*/
class MEmpSearch extends MEmp
{
public $fullname;
public $username;
/**
* @inheritdoc
*/
public function rules()
{
return [
[['id', 'user_id'], 'integer'],
[['title', 'name', 'surname','fullname','username'], 'safe'],
];
}
/**
* @inheritdoc
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = MEmp::find()->joinWith('user');
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->sort->attributes['fullname']=[
'asc'=>['name'=>SORT_ASC,'surname'=>SORT_DESC],
'desc'=>['name'=>SORT_DESC,'surname'=>SORT_DESC],
];
$dataProvider->sort->attributes['username']=[
'asc'=>['m_user.username'=>SORT_ASC],
'desc'=>['m_user.username'=>SORT_DESC],
];
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
$query->andWhere('name like "%'.$this->fullname.'%" or surname like "%'.$this->fullname.'%" ');
$query->andFilterWhere([
'id' => $this->id,
'user_id' => $this->user_id,
'm_user.username' => $this->username,
]);
// $query->andFilterWhere(['like', 'title', $this->title])
// ->andFilterWhere(['like', 'name', $this->name])
// ->andFilterWhere(['like', 'surname', $this->surname]);
return $dataProvider;
}
}
แก้ไข view/m-emp/index.php
เราจะปรับปรุงส่วนค้นหาใน GridView ขั้นตอนแรก เราจะเรียกใช้งาน fullname, username ที่เราได้สร้างไว้ที่ model MEmp
<?php
use yii\helpers\Html;
use yii\grid\GridView;
/* @var $this yii\web\View */
/* @var $searchModel backend\models\MEmpSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */
$this->title = 'Memps';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="panel panel-default">
<div class="panel-body">
<h1><?= Html::encode($this->title) ?></h1>
<?php // echo $this->render('_search', ['model' => $searchModel]); ?>
<p>
<?= Html::a('Create Memp', ['create'], ['class' => 'btn btn-success']) ?>
</p>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
//'id',
// 'title',
// 'name',
// 'surname',
// 'user_id',
'fullname',
'username',
// [
// 'attribute'=>'user_id',
// 'value'=>'user.username'
// ],
[
'class' => 'yii\grid\ActionColumn',
'options'=>['style'=>'width:120px;'],
'buttonOptions'=>['class'=>'btn btn-default'],
'template'=>'<div class="btn-group btn-group-sm text-center" role="group"> {view} {update} {delete} </div>'
],
],
]); ?>
</div>
</div>
ทดลองเพิ่มลบแก้ไข จะพบว่าเราสามารถบันทึกข้อมูลได้แล้ว และบันทึกได้พร้อมๆ กัน
ตรวจสอบความถูกต้องด้วย Transaction
เพื่อทำให้ระบบของเราน่าเชื่อถือมากขึ้นควรตรวจสอบการบันทึกข้อมูลว่าสำเร็จทุกตารางหรือไม่โดยใช้ Transaction เข้ามาช่วยตรวจสอบกระบวนการทำงานว่าถูกต้องหรือไม่
รูปแบบโครงสร้าง
use yii\base\Model;
$transaction = Model::getDb()->beginTransaction();
try {
// ...โค้ดส่วนที่เราต้องการดัก errror
$transaction->commit();
} catch (\Exception $e) {
// เมื่อมี error
$transaction->rollBack();
}
เมื่อนำมารวมกับ actionCreate() จะได้ประมาณนี้
public function actionCreate()
{
$modelEmp = new MEmp();
$modelUser = new MUser();
if($modelEmp->load(Yii::$app->request->post()) &&
$modelUser->load(Yii::$app->request->post()) &&
Model::validateMultiple([$modelEmp,$modelUser]))
{
$transaction = $modelUser::getDb()->beginTransaction();
try {
if($modelUser->save()){
$modelEmp->user_id = $modelUser->id;
$modelEmp->save();
}
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollBack();
}
return $this->redirect(['view', 'id' => $modelEmp->id]);
} else {
return $this->render('create', [
'model' => $modelEmp,
'modelUser'=>$modelUser
]);
}
}
เพิ่ม relation เองโดยให้ model จัดการ
เป็นการใช้ความสามารถของ model คือ link() โดยที่เราไม่ต้องตั้งค่า id เพื่อให้มัน relation กัน เราสามารถเรียกใช้ link() แทนเดียวมันทำให้เอง
ปกติ
// ...
if($modelUser->save()){
$modelEmp->user_id = $modelUser->id;
$modelEmp->save();
}
//...
ใช้ความสามารถของ model โดยใช้ link() (ดูเพิ่มเติม)
// ...
if($modelUser->save()){
$modelEmp->link('user',$modelUser); // easy way saving relations
}
//...
สรุป
หลักการของการบันทึกทีละหลายๆ model ในฟอร์มเดียว มีแค่ 3 จุดคือ
- ต้องสร้าง object model ที่เราต้องการใช้แล้วส่งไปแสดงผลที่ฟอร์ม
- ในตอนรับข้อมูลจาการ submit ต้องใช้ load() แยกกัน ส่วน validate ใช้ Model::multipleValidate() ร่วมกัน
- บันทึกตารางหลักก่อนแล้วค่อยบันทึกตารางรองอื่นๆ ตามลำดับ
คิดว่าคงทำให้เราพอเข้าใจหลักการทำงานเบื้องต้นและสามารถนำไปประยุกต์ใช้ในงานแบบต่างๆ ได้ นะครับ ^ ^
ตัวอย่าง SourceCode
@dixonsatit