Я пытаюсь воспользоваться функцией 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));
Есть ли способ достичь этого, сохраняя при этом транзакции?
Обновить
Ваше новое требование сохранять также посты, у которых нет ассоциированных авторов, сильно меняет ситуацию, как уже упоминалось в комментариях. Методы сохранения модели 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;
}
Вам нужно использовать 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));
?>
После бита «использовать транзакции вручную», предложенного 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'));
}