【问题标题】:Avoid duplicates by associating to inserted records with CakePHP saveMany通过使用 CakePHP saveMany 关联插入的记录来避免重复
【发布时间】:2014-11-10 10:21:39
【问题描述】:

我正在尝试利用 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));

有什么方法可以实现这一点,同时保持交易?

【问题讨论】:

  • 你能展示一个生成数据的样本来保存吗?另外,当主要模型数据属于帖子时,您为什么要通过Author 模型保存?附言。请始终提及您的确切 CakePHP 版本并相应地标记您的问题!
  • 您正在插入新行,因为您没有要保存到的主模型的主键。有 id 的数据是编辑,没有的是插入。
  • @ndm 通过作者模型保存是一个错字。我正在保存新行,但它应该只发生一次。在插入(刚刚插入的)用户之后,查询应该选择用户已经存在于数据库中,并使用 ID 而不是复制用户。我相信我需要嵌套事务,但不确定是否有更好的方法。
  • 所以你有一个Author hasMany Posts 关联?而您真正想做的是保存许多帖子,其中多个帖子可以属于单个(不一定是现有的)用户?

标签: php cakephp cakephp-2.5


【解决方案1】:

更新

您还需要保存没有关联作者的帖子的新要求极大地改变了这种情况,如 cmets 中所述,如果不是关联,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;
}

【讨论】:

  • 我的系统需要 author_id 也为 NULL。这个解决方案允许吗?即 $authors[]['Post'] = array('title' => 'foo', 'content' => 'bar');
  • @Hal9k 我不太明白你的意思,当外键为 NULL 时,没有关联,但是你想要保存关联...什么你指的到底是什么情况?
  • @Hal9k 你是说可能有没有author_username字段的行,即帖子不一定要有关联作者?
  • 感谢您的 cmets。你是对的,可能有没有作者用户名的帖子。我已经修改了原始帖子中的代码以更清楚地反映这一点。
  • @Hal9k 现在情况发生了很大变化,CakePHP 的保存方法不能同时保存不同的(非关联的)模型。如果您需要在事务中完成此操作,则必须手动处理。我会尽快更新我的答案。
【解决方案2】:

您需要使用 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));

?>

【讨论】:

    【解决方案3】:

    按照 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'));
    
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-10-15
      • 1970-01-01
      • 2017-07-12
      • 1970-01-01
      • 1970-01-01
      • 2014-05-07
      • 1970-01-01
      相关资源
      最近更新 更多