Избегайте дублирования, связываясь со вставленными записями с помощью CakePHP saveMany

Я пытаюсь воспользоваться функцией saveMany в CakePHP (со связанной функцией данных), однако создаю дублирующиеся записи. Я думаю, это потому, что запрос 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));

Есть ли способ достичь этого, сохраняя при этом транзакции?

2

Решение

Обновить

Ваше новое требование сохранять также посты, у которых нет ассоциированных авторов, сильно меняет ситуацию, как уже упоминалось в комментариях. Методы сохранения модели CakePHP не позволяют сохранять данные сразу из разных моделей, если это не ассоциация, если вы нужно сделать это в транзакции, тогда вам придется обрабатывать это вручную.

Сохранить авторов и их сообщения вместо сообщений и их авторов

Я бы посоветовал вам сохранять данные наоборот, то есть сохранять авторов и связанные с ними сообщения, чтобы вы могли легко позаботиться о дублирующих пользователях, просто сгруппировав их данные, используя имя пользователя.

Таким образом, 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;
}
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));

?>
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'));

}
0