具有活动记录的yii2中的复杂数据库查询

时间:2017-01-10 17:39:56

标签: php mysql activerecord yii yii2

TL; DR 我有一个在RAW SQL中工作的查询,但我在使用查询构建器或活动记录重新创建它时收效甚微。

我正在开发基于yii2高级应用程序模板的Web应用程序。我编写了一个数据库查询并使用findbysql()实现它,返回正确的记录但是在将其转换为活动记录时遇到问题。

我原本希望允许用户通过搜索表单(用户和日期)修改(过滤)结果,但是我已经意识到在gridview上使用活动记录实现过滤器会更顺畅。

我已经获得了简单的查询,但不确定如何使用这么多连接实现一个。许多示例使用了子查询,但我的尝试根本没有返回任何记录。我想在尝试过滤器之前我需要先转录这个查询。

videoController.php

<div class="slideshow-container">

  <div class="mySlides fade">
    <div class="numbertext">1 / 6</div>
    <img src="gal/a1.jpg" style="width:100%">
    <div class="text">Our Mission</div>
  </div>

  <div class="mySlides fade">
    <div class="numbertext">2 / 6</div>
    <img src="gal/a2.jpg" style="width:100%">
    <div class="textwbg">Our Doctor Pannel</div>
  </div>

  <div class="mySlides fade">
    <div class="numbertext">3 / 6</div>
    <img src="gal/a3.jpg" style="width:100%">
    <div class="textwbg">Make an Appointment</div>
  </div>

  <div class="mySlides fade">
    <div class="numbertext">4 / 6</div>
    <img src="gal/a4.jpg" style="width:100%">
    <div class="text">Friendly Environment</div>
  </div>

  <div class="mySlides fade">
    <div class="numbertext">5 / 6</div>
    <img src="gal/a5.jpg" style="width:100%">
    <div class="textwbg">24/7</div>
  </div>

  <div class="mySlides fade">
    <div class="numbertext">6 / 6</div>
    <img src="gal/a6.jpg" style="width:100%">
    <div class="textwbg">Facilities</div>
  </div>

  <a class="prev" onclick="plusSlides(-1)">O</a>
  <a class="next" onclick="plusSlides(1)">O</a>

</div>
<br>

<div style="text-align:center">

  <span class="dot" onclick="currentSlide(1)"></span> 
  <span class="dot" onclick="currentSlide(2)"></span> 
  <span class="dot" onclick="currentSlide(3)"></span> 
  <span class="dot" onclick="currentSlide(4)"></span> 
  <span class="dot" onclick="currentSlide(5)"></span> 
  <span class="dot" onclick="currentSlide(6)"></span> 

</div>

尝试失败

public function actionIndex()
{

    $sql =  'SELECT videos.idvideo, videos.filelocation, events.event_type, events.event_timestamp
                    FROM (((ispy.videos videos
                        INNER JOIN ispy.cameras cameras
                            ON (videos.cameras_idcameras = cameras.idcameras))
                        INNER JOIN ispy.host_machines host_machines
                            ON (cameras.host_machines_idhost_machines =
                                    host_machines.idhost_machines))
                        INNER JOIN ispy.events events
                            ON (events.host_machines_idhost_machines =
                                    host_machines.idhost_machines))
                        INNER JOIN ispy.staff staff
                            ON (events.staff_idreceptionist = staff.idreceptionist)
                    WHERE     (staff.idreceptionist = 182)
                            AND (events.event_type IN (23, 24))
                            AND (events.event_timestamp BETWEEN videos.start_time
                                   AND videos.end_time)';
        $query = Videos::findBySql($sql);

    $dataProvider = new ActiveDataProvider([
        'query' =>  $query,
    ]);

    return $this->render('index', [
        'dataProvider' => $dataProvider,
    ]);

}

视图的一部分

public function actionIndex()
{
    $query = Videos::find()
    ->innerJoin('cameras',  'videos.cameras_idcameras = cameras.idcameras')
    ->innerJoin('host_machines',  'cameras.host_machines_idhost_machines = host_machines.idhost_machines')
    ->innerJoin('events',  'events.host_machines_idhost_machines =  host_machines.idhost_machines')
    ->innerJoin('staff',  'events.staff_idreceptionist = staff.idreceptionist')
    ->where('staff.idreceptionist = 182')
    ->andWhere(['events.event_type' => [23,24]])
    ->andwhere(['between', 'events.event_timestamp', 'videos.start_time', 'videos.end_time']);


    $dataProvider = new ActiveDataProvider([
        'query' =>  $query,
    ]);

    return $this->render('index', [
        'dataProvider' => $dataProvider,
    ]);

}

如果我需要更具体或包含任何其他信息,请告诉我。

提前致谢

2 个答案:

答案 0 :(得分:2)

我将根据您在评论中提到here的问题假设您提供了整个查询 (没有其他字段,您只是为了展示示例代码而拿出来)

因此,如果您只需要SELECT语句中指定的字段,则可以优化您的查询:

首先,您加入host_machines仅加入camerasevents,但两者都有相同的密钥host_machines_idhost_machines,以便&#39 ; s不需要,你可以直接:

    INNER JOIN events events
        ON (events.host_machines_idhost_machines =
            cameras.host_machines_idhost_machines))

其次,与ispy.staff的联接,唯一使用的字段是idreceptionist子句中的WHERE,该字段也存在于events中,因此我们可以完全删除< / p>

这里的最终查询:

SELECT videos.idvideo, videos.filelocation, events.event_type, events.event_timestamp
FROM videos videos
    INNER JOIN cameras cameras
        ON videos.cameras_idcameras = cameras.idcameras
    INNER JOIN events events
        ON events.host_machines_idhost_machines =
                cameras.host_machines_idhost_machines
WHERE     (events.staff_idreceptionist = 182)
        AND (events.event_type IN (23, 24))
        AND (events.event_timestamp BETWEEN videos.start_time
               AND videos.end_time)

应该输出与你问题中的记录相同的记录,没有任何标识行 由于camerasevents

之间的一对多关系,某些视频副本仍然存在

现在到了yii方面的事情,
你必须在视频模型

上定义一些关系
// this is pretty straight forward, `videos`.`cameras_idcameras` links to a 
// single camera (one-to-one)
public function getCamera(){
    return $this->hasOne(Camera::className(), ['idcameras' => 'cameras_idcameras']);
}
// link the events table using `cameras` as a pivot table (one-to-many)
public function getEvents(){
    return $this->hasMany(Event::className(), [
        // host machine of event        =>  host machine of camera (from via call)
        'host_machines_idhost_machines' => 'host_machines_idhost_machines'
    ])->via('camera');
}

VideoController 和搜索功能本身

public function actionIndex() {
    // this will be the query used to create the ActiveDataProvider
    $query =Video::find()
        ->joinWith(['camera', 'events'], true, 'INNER JOIN')
        ->where(['event_type' => [23, 24], 'staff_idreceptionist' => 182])
        ->andWhere('event_timestamp BETWEEN videos.start_time AND videos.end_time');

    $dataProvider = new ActiveDataProvider([
        'query' =>  $query,
    ]);

    return $this->render('index', [
        'dataProvider' => $dataProvider,
    ]);
}

yii会将每个视频视为单个记录(基于pk),这意味着所有视频副本都是 除去。您将拥有单个视频,每个视频都有多个活动,因此您将无法使用'event_type' 和视图中的'event_timestamp',但您可以在视频模型中声明一些getter来显示该信息:

public function getEventTypes(){
    return implode(', ', ArrayHelper::getColumn($this->events, 'event_type'));
}

public function getEventTimestamps(){
    return implode(', ', ArrayHelper::getColumn($this->events, 'event_timestamp'));
}

并且视图使用:

<?= GridView::widget([
    'dataProvider' => $dataProvider,
    'columns' => [
        ['class' => 'yii\grid\SerialColumn'],
        'idvideo',
        'eventTypes',
        'eventTimestamps',
        'filelocation',
        //['class' => 'yii\grid\ActionColumn'],
    ],
]); ?>

修改
如果您想保持视频重复,请在视频模型

中声明events中的两列
public $event_type, $event_timestamp;

保留原始GridView设置,并将selectindexBy添加到 VideoController 中的查询中:

$q  = Video::find()
    // spcify fields
    ->addSelect(['videos.idvideo', 'videos.filelocation', 'events.event_type', 'events.event_timestamp'])
    ->joinWith(['camera', 'events'], true, 'INNER JOIN')
    ->where(['event_type' => [23, 24], 'staff_idreceptionist' => 182])
    ->andWhere('event_timestamp BETWEEN videos.start_time AND videos.end_time')
    // force yii to treat each row as distinct
    ->indexBy(function () {
        static $count;
        return ($count++);
    });

更新

staff的直接Video关系目前有些问题,因为距离它不止一张桌子。 关于它的问题是here

但是,您可以通过将staff表格与事件模型相关联来添加该表格,

public function getStaff() {
    return $this->hasOne(Staff::className(), ['idreceptionist' => 'staff_idreceptionist']);
}

允许您像这样查询:

->joinWith(['camera', 'events', 'events.staff'], true, 'INNER JOIN')

过滤将需要控制器上的一些小更新,查看和SarchModel
这是一个最小的实现:

class VideoSearch extends Video
{
    public $eventType;
    public $eventTimestamp;
    public $username;

    public function rules() {
        return array_merge(parent::rules(), [
            [['eventType', 'eventTimestamp', 'username'], 'safe']
        ]);
    }

    public function search($params) {
        // add/adjust only conditions that ALWAYS apply here:
        $q = parent::find()
            ->joinWith(['camera', 'events', 'events.staff'], true, 'INNER JOIN')
            ->where([
                'event_type' => [23, 24],
                // 'staff_idreceptionist' => 182
                // im guessing this would be the username we want to filter by
            ])
            ->andWhere('event_timestamp BETWEEN videos.start_time AND videos.end_time');

        $dataProvider = new ActiveDataProvider(['query' => $q]);

        if (!$this->validate())
            return $dataProvider;

        $this->load($params);

        $q->andFilterWhere([
            'idvideo'                => $this->idvideo,
            'events.event_type'      => $this->eventType,
            'events.event_timestamp' => $this->eventTimestamp,
            'staff.username'         => $this->username,
        ]);

        return $dataProvider;
    }
}

控制器:

public function actionIndex() {
    $searchModel = new VideoSearch();
    $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

    return $this->render('test', [
        'searchModel'  => $searchModel,
        'dataProvider' => $dataProvider,
    ]);
}

和视图

use yii\grid\GridView;
use yii\helpers\ArrayHelper;

echo GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel'  => $searchModel,
    'columns'      => [
        ['class' => 'yii\grid\SerialColumn'],
        'idvideo',
        'filelocation',
        [
            'attribute' => 'eventType',     // from VideoSearch::$eventType (this is the one you filter by)
            'value'     => 'eventTypes'     // from Video::getEventTypes() that i suggested yesterday
            // in hindsight, this could have been named better, like Video::formatEventTypes or smth
        ],
        [
            'attribute' => 'eventTimestamp',
            'value'     => 'eventTimestamps'
        ],
        [
            'attribute' => 'username',
            'value'     => function($video){
                return implode(', ', ArrayHelper::map($video->events, 'idevent', 'staff.username'));
            }
        ],
        //['class' => 'yii\grid\ActionColumn'],
    ],
]);

答案 1 :(得分:1)

我的建议是进行2次查询。第一个获取适合您搜索的视频的ID,第二个查询使用这些ID并向您$dataProvider提供。

use yii\helpers\ArrayHelper;

...

public function actionIndex()
{
    // This is basically the same query you had before
    $searchResults = Videos::find()
        // change 'id' for the name of your primary key
        ->select('id')
        // we don't really need ActiveRecord instances, better use array
        ->asArray()
        ->innerJoin('cameras', 'videos.cameras_idcameras = cameras.idcameras')
        ->innerJoin('host_machines', 'cameras.host_machines_idhost_machines = host_machines.idhost_machines')
        ->innerJoin('events', 'events.host_machines_idhost_machines =  host_machines.idhost_machines')
        ->innerJoin('staff', 'events.staff_idreceptionist = staff.idreceptionist')
        ->where('staff.idreceptionist = 182')
        ->andWhere(['events.event_type' => [23,24]])
        ->andwhere(['between', 'events.event_timestamp', 'videos.start_time', 'videos.end_time'])
        // query the results
        ->all();

    // this will be the query used to create the ActiveDataProvider
    $query = Videos::find()
        // and we use the results of the previous query to filter this one
        ->where(['id' => ArrayHelper::getColumn($searchResults, 'id')]);

    $dataProvider = new ActiveDataProvider([
        'query' =>  $query,
    ]);

    return $this->render('index', [
        'dataProvider' => $dataProvider,
    ]);
}