草庐IT

php - Zend framework 2/Doctrine 2/批量操作和事件触发

coder 2024-04-19 原文

对于一个包含很多实体的大型项目,我编写了一个save() 通用方法。

此方法存储在抽象服务中,并在所有项目中用于保存实体状态。

AbstractService::save() 看起来像这样:

public function save($entity)
{
    $transactionStarted = $this->beginTransaction();

    try
    {
        $action = $entity->getId() ? self::UPDATE : self::CREATION;

        $this->getEventManager()->trigger('save.pre', $entity, ['action' => $action]);

        $this->getEntityManager()->persist($entity);
        $this->getEntityManager()->flush();

        $this->getEventManager()->trigger('save.post', $entity, ['action' => $action]);

        if ($transactionStarted)
        {
            $this->commitTransaction();
        }
    } catch (\Exception $e)
    {
        if ($transactionStarted)
        {
            $this->rollbackTransaction();
        }

        throw new Exception('Unable to save entity', $e);
    }

    return true;
}

public function beginTransaction()
{
    if (!$this->getEntityManager()->getConnection()->isTransactionActive())
    {
        $this->getEntityManager()->getConnection()->beginTransaction();

        return true;
    }

    return false;
}

public function commitTransaction()
{
    $this->getEntityManager()->getConnection()->commit();

    return $this;
}

public function rollbackTransaction()
{
    $this->getEntityManager()->getConnection()->rollBack();

    return $this;
}

在我的例子中,当调用Member服务(扩展的AbstractService)时插入一个成员(新的Member实体),一封电子邮件通过(例如)save.post 事件发送。 或者也可以继续与调用保存方法的另一个服务相关的另一个操作。

“子”MemberService::save() 方法的示例

MemberService

public function save(Member $member)
{
    // some stuff, e.g set a property
    $member->setFirstName('John');

    return parent::save($member);
}

触发事件示例

$sharedEventManager->attach(MemberService::class, 'save.post', [$this, 'onMembersCreation']);

public function onMembersCreation(EventInterface $event)
{
    // send an email

    // anything else ... update another entity ... (call AnotherService::save() too) 
}

这对于简单的保存过程来说非常有用。

但现在,我想大量导入大量成员,包括创建、更新……为了实现这一点,我阅读了与批量导入相关的 Doctrine 文档。 Doc here

但是如何正确更新我的代码以处理“批量保存”和“单一保存”?并保持交易安全和事件?

最佳答案

基本上我建议你实现 Doctrine\Common\Collections\Collection 接口(interface),也许扩展 ArrayCollection,并创建一个方法 save 来完成文档告诉你的事情。

<?php

class MyDirtyCollection extends \Doctrine\Common\Collections\ArrayCollection {

    public function __construct(AbstractService $abstractService)
    {
        $this->service = $abstractService;
    }

    public function save()
    {
        foreach ($this as $entity) {
            $this->service->save($entity);
        }
    }
}

class MyCollection extends \Doctrine\Common\Collections\ArrayCollection {

    public $bulkSize = 500;

    protected $eventManager;
    protected $entityManager;

    public function __construct(EntityManager $entityManager, EventManager $eventManager)
    {
        $this->entityManager = $entityManager;
        $this->eventManager = $eventManager;
    }

    public function getEventManager()
    {
        return $this->eventManager;
    }

    public function getEntityManager()
    {
        return $this->entityManager;
    }

    public function setBulkSize(int $bulkSize)
    {
        $this->bulkSize = $bulkSize;
    }

    public function save()
    {
        $transactionStarted = $this->getEntityManager()->getConnection()->beginTransaction();

        try {
            foreach ($this as $entity) {
                $action = $entity->getId() ? self::UPDATE : self::CREATION;
                $this->getEventManager()->trigger('save.pre', $entity, ['action' => $action]);
            }

            $i = 0;
            foreach ($this as $entity) {
                $i++;

                $this->getEntityManager()->persist($entity);

                if (($i % $this->bulkSize) === 0) {
                    $this->getEntityManager()->flush();
                    $this->getEntityManager()->clear();
                }
            }

            $this->getEntityManager()->flush();
            $this->getEntityManager()->clear();

            foreach ($this as $entity) {
                $action = $entity->getId() ? self::UPDATE : self::CREATION;
                $this->getEventManager()->trigger('save.post', $entity, ['action' => $action]);
            }

            if ($transactionStarted) {
                $this->getEntityManager()->getConnection()->commitTransaction();
            }

        } catch (Exception $e) {
            $this->getEntityManager()->rollbackTransaction();
        }
    }
}

类似的东西 ;) 当您获取数据时,您会滋润您的集合,然后处理您的实体,最后调用 $collection->save();

编辑:在下面添加插入类和用例:

这里的性能会比较低,但还是比commit by commit要好。然而,如果您正在寻找 hgih 性能,您应该考虑使用 Doctrine DBAL 而不是 ORM。在这里,我与您分享我用于批量插入的 DBAL 类:

<?php

namespace JTH\Doctrine\DBAL;

use Doctrine\DBAL\Query\QueryBuilder;
use Exception;
use InvalidArgumentException;
use Traversable;
use UnderflowException;

class Insert extends QueryBuilder
{
    const CALLBACK_FAILURE_SKIP = 0;
    const CALLBACK_FAILURE_BREAK = 1;

    protected $callbackFailureStrategy = self::CALLBACK_FAILURE_BREAK;

    public static $defaultBulkSize = 500;

    public $ignore = false;
    public $onDuplicate = null;

    public function values(array $values)
    {
        $this->resetQueryPart('values');
        $this->addValues($values);
    }

    public function addValues(array $values)
    {
        $this->add('values', $values, true);
    }

    public function setCallbackFailureStrategy($strategy)
    {
        if ($strategy == static::CALLBACK_FAILURE_BREAK) {
            $this->callbackFailureStrategy = static::CALLBACK_FAILURE_BREAK;
        } elseif ($strategy == static::CALLBACK_FAILURE_SKIP) {
            $this->callbackFailureStrategy = static::CALLBACK_FAILURE_SKIP;
        } else {
            $class = self::class;
            throw new InvalidArgumentException(
                "Invalid failure behaviour. See $class::CALLBACK_FAILURE_SKIP and $class::CALLBACK_FAILURE_BREAK"
            );
        }
    }

    public function getCallbackFailureStrategy()
    {
        return $this->callbackFailureStrategy;
    }

    public function execute()
    {
        return $this->getConnection()->executeUpdate(
            $this->getSQLForInsert(),
            $this->getParameters(),
            $this->getParameterTypes()
        );
    }

    /**
     * Converts this instance into an INSERT string in SQL.
     * @return string
     * @throws \Exception
     */
    private function getSQLForInsert()
    {
        $count = sizeof($this->getQueryPart('values'));

        if ($count == 0) {
            throw new UnderflowException("No values ready for INSERT");
        }

        $values = current($this->getQueryPart('values'));
        $ignore = $this->ignore ? 'IGNORE' : '' ;
        $sql = "INSERT $ignore INTO " . $this->getQueryPart('from')['table'] .
            ' (' . implode(', ', array_keys($values)) . ')' . ' VALUES ';

        foreach ($this->getQueryPart('values') as $values) {
            $sql .= '(' ;

            foreach ($values as $value) {
                if (is_array($value)) {
                    if ($value['raw']) {
                        $sql .= $value['value'] . ',';
                    } else {
                        $sql .= $this->expr()->literal($value['value'], $value['type']) . ',';
                    }
                } else {
                    $sql .= $this->expr()->literal($value) . ',';
                }
            }

            $sql = substr($sql, 0, -1);
            $sql .= '),';
        }

        $sql = substr($sql, 0, -1);

        if (!is_null($this->onDuplicate)) {
            $sql .= ' ON DUPLICATE KEY UPDATE ' . $this->onDuplicate . ' ';
        }

        return $sql;
    }

    /**
     * @param $loopable array | Traversable An array or object to loop over
     * @param $callable Callable A callable that will be called before actually insert the row.
     * two parameters will be passed :
     * - the key of the current row
     * - the row values (Array)
     * An array of rows to insert must be returned
     * @param $bulkSize int How many rows will be inserted at once
     * @param bool $transactionnal
     * @throws \Doctrine\DBAL\ConnectionException
     * @throws \Exception
     */
    public function bulk($loopable, callable $callable, $bulkSize = null, $transactionnal = true)
    {
        if (!is_array($loopable) and !($loopable instanceof Traversable)) {
            throw new InvalidArgumentException("\$loppable must be either an array or a traversable object");
        }

        $bulkSize = $bulkSize ?? static::$defaultBulkSize;

        $this->getConnection()->getConfiguration()->setSQLLogger(null); // Avoid MonoLog memory overload

        if ($transactionnal) {
            $this->getConnection()->beginTransaction();
        }

        $this->resetQueryPart('values');

        foreach ($loopable as $key => $values) {
            try {
                $callbackedValues = $callable($key, $values);

                if (sizeof($callbackedValues) > 0) {
                    foreach ($callbackedValues as $callbackedValuesRow) {
                        $this->addValues($callbackedValuesRow);
                    }
                }
            } catch (Exception $e) {
                /*
                 * If a callback exception must break the transaction, then throw the exception to the call stack
                 * Else, skip the row insertion
                 */
                if ($this->callbackFailureStrategy == static::CALLBACK_FAILURE_BREAK) {
                    throw $e;
                } else {
                    continue;
                }
            }

            $count = count($this->getQueryPart('values'));

            if ($count >= $bulkSize) {
                $this->execute();
                $this->resetQueryPart('values');
            }
        }

        $count = count($this->getQueryPart('values'));

        if ($count > 0) {
            $this->execute();
        }

        $this->resetQueryPart('values');

        if ($transactionnal) {
            $this->getConnection()->commit();
        }
    }

    /**
     * @return boolean
     */
    public function isIgnore()
    {
        return $this->ignore;
    }

    /**
     * @param boolean $ignore
     */
    public function setIgnore(bool $ignore)
    {
        $this->ignore = $ignore;
    }

    /**
     * @return null|string
     */
    public function getOnDuplicate() : string
    {
        return $this->onDuplicate;
    }

    /**
     * @param null $onDuplicate
     */
    public function setOnDuplicate($onDuplicate)
    {
        $this->onDuplicate = $onDuplicate;
        $this->ignore = false;
    }


}

用例:

    try {
        $i = new Insert($this->getDoctrine()->getConnection('myDB'));
        $i->insert('myTable');
        $i->setOnDuplicate('col1 = VALUES(col1), updated_last = NOW()');
        $i->setCallbackFailureStrategy(Insert::CALLBACK_FAILURE_BREAK);
        $i->bulk($myArrayOfRows, function ($key, $row) {

            // Some pre-insert processing

            $rowset[] = $row;

            return $rowset;

        }, 500, true);

        $this->addFlash('success', 'Yay !');

    } catch (DBALException $e) {
        $this->addFlash('error', 'Damn, error : ' . $e->getMessage());
    }

关于php - Zend framework 2/Doctrine 2/批量操作和事件触发,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36390063/

有关php - Zend framework 2/Doctrine 2/批量操作和事件触发的更多相关文章

  1. ruby - 如何批量检查文件内容是否相同 - 2

    我想使用Ruby检查数千对文件中的每对文件是否包含相同的信息。有人能指出我正确的方向吗? 最佳答案 require'fileutils'FileUtils.compare_file('file1','file2')当且仅当文件file1和file2相同时返回true。 关于ruby-如何批量检查文件内容是否相同,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/33769865/

  2. ruby-on-rails - 这个 C 和 PHP 程序员如何学习 Ruby 和 Rails? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我来自C、php和bash背景,很容易学习,因为它们都有相同的C结构,我可以将其与我已经知道的联系起来。然后2年前我学了Python并且学得很好,Python对我来说比Ruby更容易学。然后从去年开始,我一直在尝试学习Ruby,然后是Rails,我承认,直到现在我还是学不会,讽刺的是那些打着简单易学的烙印,但是对于我这样一个老练的程序员来说,我只是无法将它

  3. ruby-on-rails - 在 Rails 中使用 accepts_nested_attributes_for + 批量赋值保护 - 2

    假设你有这个结构:classHouse请注意,Tv的用户是故意不可访问的。所以你有一个三层嵌套的表单,允许你在一个页面上输入房子、房间和电视。这是Controller的创建方法:defcreate@house=House.new(params[:house])if@house.save#...standardstuffelse#...standardstuffendend问题:您究竟如何为每台电视填充user_id(它应该来自current_user.id)?什么是好的做法?这是我在其中看到的catch22。将user_ids直接填充到params散列中(它们嵌套得很深)保存将失败,因

  4. ruby - 如何使用 bash 命令或 Ruby 使用 ffmpeg 将 mp4 文件批量转换为 ogg - 2

    我运行的是OSX,对视频转换一无所知。但我有大约200个视频都是mp4格式,无法在Firefox中播放。我需要将它们转换为ogg才能使用html5视频标签。这些文件位于一个文件夹结构中,这使得一次一个地处理一个文件变得困难。我希望bash命令或Ruby命令遍历所有子文件夹并找到所有.mp4并转换它们。我找到了一份关于如何使用Google执行此操作的引用资料:http://athmasagar.wordpress.com/2011/05/12/a-bash-script-to-convert-mp4-files-to-oggogv/#!/bin/bashforfin$(ls*mp4|se

  5. ruby-on-rails - Rails 还是 Sinatra? PHP程序员入门学习哪个好? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭10年前。我使用PHP的时间太长了,对它感到厌倦了。我也想学习一门新语言。我一直在使用Ruby并且喜欢它。我必须在Rails和Sinatra之间做出选择,那么您会推荐哪一个?Sinatra真的不能用来构建复杂的应用程序,它只能用于简单的应用程序吗?

  6. sql - 是否可以在 Sequel 中批量更新? - 2

    是否可以使用Sequel在一次调用中进行多次更新??例如,在我的服务器上进行大约200次更新可能需要几分钟,但如果我伪造一个SQL查询,它会在几秒钟内运行。我想知道Sequel是否可以用来伪造那个SQL查询,或者更好的是,自己完成整个操作。 最佳答案 我遇到的解决方案涉及update_sql方法。它不是自己执行操作,而是输出原始SQL查询。要批量更新多个更新,只需将它们与;连接起来即可。在此期间,使用结果字符串调用run方法,一切就绪。批处理解决方案比多次更新快得多。 关于sql-是否可

  7. ruby-on-rails - PHP 魔术方法 __call、__get 和 __set 的 Ruby 等价物 - 2

    我很确定Ruby有这些(等同于__call、__get和__set),否则find_by将如何在Rails中工作?也许有人可以举一个简单的例子来说明如何定义与find_by相同的方法?谢谢 最佳答案 简而言之你可以映射__调用带有参数的method_missing调用__设置为方法名称以'='结尾的method_missing调用__获取不带任何参数的method_missing调用__调用PHPclassMethodTest{publicfunction__call($name,$arguments){echo"Callingob

  8. ruby - Lisp - 是否适合网络编程/应用程序(交互式)? ruby 的方式是? php的方式是? - 2

    Lisp是否适合Web编程/应用程序(交互式),就像ruby​​和php一样?需要考虑的事情是:易于使用可部署性难度(尤其是对于编程初学者而言)(编辑)在阅读PaulGraham'sessay之后,我特别提到了CommonLisp.将是我的第一门编程语言。在这方面。这样做合适吗?我听说Clojure的宏功能不如CommonLisp的强大,这就是我尝试学习Clojure的原因。它教授编程并且非常强大。 最佳答案 Lisp是一个语系,而不是单一的语言。为了稍微回答您的问题,是的,存在用于各种Lisp方言的Web框架,例如用于Common

  9. ruby-on-rails - Rails 3.2,批量分配,动态角色? - 2

    我有一个Rails应用,其用户模型包含一个admin属性。它使用attr_accessible锁定。我的模型如下所示:attr_accessible:name,:email,:other_email,:plant_id,:password,:password_confirmationattr_accessible:name,:email,:other_email,:plant_id,:password,:password_confirmation,:admin,:as=>:admin下面是我的用户Controller中的更新方法:defupdate@user=User.find(par

  10. 软件工程毕业设计课题(81)微信小程序毕业设计PHP校园跑腿小程序系统设计与实现 - 2

        项目背景和意义 目的:本课题主要目标是设计并能够实现一个基于微信校园跑腿小程序系统,前台用户使用小程序发布跑腿任何和接跑腿任务,后台管理使用基于PHP+MySql的B/S架构;通过后台管理跑腿的用户、查看跑腿信息和对应订单。意义:手机网络时代,大学生通过手机网购日常用品、外卖外卖、代取快递等已不再是稀奇的事情。此外,不少高校还流行着校园有偿工作,校园跑腿就成了大学生创业服务项目。        因为你在校园里,所以不会有进入的限制。并不是所有的外卖平台都可以随意进入校园,比如小黄和小蓝的双打外卖平台。许多大学禁止送餐进入学校,更不用说送餐进入宿舍了。这一措施使得校园服务市场的竞争相对不

随机推荐