通过与CakePHP saveMany关联插入的记录来避免重复

时间:2014-11-10 10:21:39

标签: php cakephp cakephp-2.5

我正在尝试利用CakePHP的saveMany功能(具有关联的数据功能),但是我正在创建重复的记录。我认为这是因为find()查询没有找到作者,因为事务尚未提交给数据库。

这意味着如果有两个作者具有相同的用户名,例如,在电子表格中,那么CakePHP将不会将第二个与第一个相关联,而是创建两个。我为这篇文章编了一些代码:

/*
 * Foobar user (not in database) entered twice, whereas Existing user 
 * (in database) is associated
 */

$spreadsheet_rows = array(
    array(
      'title' => 'New post',
      'author_username' => 'foobar',
      'content' => 'New post'
    ),
    array(
      'title' => 'Another new post',
      'author_username' => 'foobar',
      'content' => 'Another new post'
    ),
    array(
      'title' => 'Third post',
      'author_username' => 'Existing user',
      'content' => 'Third post'
    ),
    array(
      'title' => 'Fourth post', // author_id in this case would be NULL
      'content' => 'Third post'
    ),

);


$posts = array();

foreach ($spreadsheet_rows as $row) {

    /*
     * This query doesn't pick up the authors
     * entered automatically (see comment 2.)
     * within the db transaction by CakePHP,
     * so creates duplicate author names
     */

    $author = $this->Author->find('first', array('conditions' => array('Author.username' => $row['author_username'])));

    $post = array(
        'title' => $row['title'],
        'content' => $row['content'],
    );

    /*
     * Associate post to existing author
     */

    if (!empty($author)) {
        $post['author_id'] = $author['Author']['id'];
    } else {

        /*
         * 2. CakePHP creates and automatically
         * associates new author record if author_username is not blank
         * (author_id is NULL in db if blank)
         */

        if (!empty($ow['author_username'])) {            
             $post['Author']['username'] = $row['author_username'];
        }
    }

    $posts[] = $post;
}


$this->Post->saveMany($posts, array('deep' => true));

有没有办法实现这一目标,同时还能保持交易?

3 个答案:

答案 0 :(得分:0)

<强>更新

你还要求保存没有关联作者的帖子会改变这种情况很多,正如评论中所提到的,CakePHPs模型保存方法如果它不是一个关联,就不能一次性保存来自不同模型的数据,如果您需要在交易中执行此操作,则需要手动处理此操作。

保存作者及其帖子,而不是帖子及其作者

我建议您以相反的方式保存数据,即保存作者及其相关帖子,这样您只需使用用户名对数据进行分组即可轻松处理重复用户。

围绕CakePHP的方式只会在必要时创建新作者,并自动将相应的外键添加到帖子中。

然后应将数据格式化为:

Array
(
    [0] => Array
        (
            [username] => foobar
            [Post] => Array
                (
                    [0] => Array
                        (
                            [title] => New post
                        )
                    [1] => Array
                        (
                            [title] => Another new post
                        )
                )
        )
    [1] => Array
        (
            [id] => 1
            [Post] => Array
                (
                    [0] => Array
                        (
                            [title] => Third post
                        )
                )
        )
)

您可以通过Author模式保存:

$this->Author->saveMany($data, array('deep' => true));

单独存储非关联帖子并手动使用交易

如果你想使用CakePHP ORM,就没有办法解决这个问题,想象一下如果需要处理所有逻辑,原始SQL查询需要看起来像什么。

所以只需将其拆分为两个保存,然后手动使用DboSource::begin()/commit()/rollback()将其全部包装起来。

一个例子

以下是基于您的数据的简单示例,根据您的新要求进行了更新:

$spreadsheet_rows = array(
    array(
      'title' => 'New post',
      'author_username' => 'foobar',
      'content' => 'New post'
    ),
    array(
      'title' => 'Another new post',
      'author_username' => 'foobar',
      'content' => 'Another new post'
    ),
    array(
      'title' => 'Third post',
      'author_username' => 'Existing user',
      'content' => 'Third post'
    ),
    array(
      'title' => 'Fourth post',
      'content' => 'Fourth post'
    ),
    array(
      'title' => 'Fifth post',
      'content' => 'Fifth post'
    ),
);

$authors = array();
$posts = array();
foreach ($spreadsheet_rows as $row) {
    // store non-author associated posts separately
    if (!isset($row['author_username'])) {
        $posts[] = $row;
    } else {
        $username = $row['author_username'];

        // prepare an author only once per username
        if (!isset($authors[$username])) {
            $author = $this->Author->find('first', array(
                'conditions' => array(
                    'Author.username' => $row['author_username']
                )
            ));

            // if the author already exists use its id, otherwise
            // use the username so that a new author is being created
            if (!empty($author)) {
                $authors[$username] = array(
                    'id' => $author['Author']['id']
                );
            } else {
                $authors[$username] = array(
                    'username' => $username
                );
            }
            $authors[$username]['Post'] = array();
        }

        // group posts under their respective authors
        $authors[$username]['Post'][] = array(
            'title' => $row['title'],
            'content' => $row['content'],
        );
    }
}

// convert the string (username) indices into numeric ones
$authors = Hash::extract($authors, '{s}');

// manually wrap both saves in a transaction.
//
// might require additional table locking as
// CakePHP issues SELECT queries in between.
//
// also this example requires both tables to use
// the default connection
$ds = ConnectionManager::getDataSource('default');
$ds->begin();

try {
    $result =
        $this->Author->saveMany($authors, array('deep' => true)) &&
        $this->Post->saveMany($posts);

    if ($result && $ds->commit() !== false) {
        // success, yay
    } else {
        // failure, buhu
        $ds->rollback();
    }
 } catch(Exception $e) {
    // failed hard, ouch
    $ds->rollback();
    throw $e;
}

答案 1 :(得分:0)

您需要使用saveAll,它是saveMany和saveAssociated之间的混合(您需要在这里执行这两个操作)。 另外,您需要更改每个帖子的结构。

以下是您需要在循环内创建的结构示例。

<?php
  $posts = array();

  //This is a post for a row with a new author
  $post = array (
    'Post' => array ('title' => 'My Title', 'content' => 'This is the content'),
    'Author' => array ('username' => 'new_author')
  );
  $posts[] = $post;

  //This is a post for a row with an existing author
  $post = array (
    'Post' => array ('title' => 'My Second Title', 'content' => 'This is another content'),
    'Author' => array ('id' => 1)
  );
  $posts[] = $post;

  //This is a post for a row with no author
  $post = array (
    'Post' => array ('title' => 'My Third Title', 'content' => 'This is one more content')
  );
  $posts[] = $post;


  $this->Post->saveAll($posts, array ('deep' => true));

?>

答案 2 :(得分:0)

在ndm建议的“手动使用事务”位之后,这段代码(用单元测试编写!)似乎可以解决问题:

public function testAdd() {
    $this->generate('Articles', array());

    $this->controller->loadModel('Article');
    $this->controller->loadModel('Author');

    $csv_data = array(
        array(
            'Article' => array(
                'title' => 'title'
            )),
        array(
            'Article' => array(
                'title' => 'title'
            ),
            'Author' => array(
                'name' => 'foobar'
            ),

        ),
        array(
            'Article' => array(
                'title' => 'title2'
            ),
            'Author' => array(
                'name' => 'foobar'
            )
        ),
        /* array( */
        /*     'Article' => array( */
        /*         'title' => '' */
        /*     ), */
        /*     'Author' => array( */
        /*         'name' => '' // this breaks our validation */
        /*     ) */
        /* ), */
    );

    $db = $this->controller->Article->getDataSource();

    $db->begin();

    /*
     * We want to inform the user of _all_ validation messages, not one at a time
     */

    $validation_errors = array();

    /*
     * Do this by row count, so that user can look through their CSV file
     */

    $row_count = 1;

    foreach ($csv_data as &$row) {

        /*
         * If author already exists, don't create new record, but associate to existing
         */

        if (!empty($row['Author'])) {                
            $author = $this->controller->Author->find('first', 
                array(
                    'conditions' => array(
                        'name' => $row['Author']['name']
                    )
                ));

            if (!empty($author)) {
                $row['Author']['id'] = $author['Author']['id'];
            }
        }

        $this->controller->Article->saveAssociated($row, array('validate' => true));

        if (!empty($this->controller->Article->validationErrors)) {
            $validation_errors[$row_count] = $this->controller->Article->validationErrors;
        }            
        $row_count++;
    }


    if (empty($validation_errors)) {
        $db->commit();            
    } else {
        $db->rollback();
        debug($validation_errors);
    }

    debug($this->controller->Article->find('all'));

}