Symfony/Doctrine/PHP:无需数组存储获取持久化对象的优化方案
嘿,这个问题我之前处理批量数据导入的时候也碰到过,给你几个靠谱的解决方案,不用改php.ini的内存限制,也不需要自己维护那个占内存的$newCompanies数组:
其实你自己维护的$newCompanies数组完全是多余的——Doctrine的EntityManager本身就会在**一级缓存(UnitOfWork)**里跟踪所有已经加载或者persist过的实体。也就是说,当你persist一个新的Company但还没flush的时候,调用findOneBy会先检查这个缓存,直接返回你刚创建的实体,根本不用自己存数组。
修改后的代码会简洁很多:
foreach ($rows as $row) { $companyName = // 从当前行提取公司名称 // 先查数据库+一级缓存(包含已persist未flush的实体) $company = $this->entityManager()->getRepository(Company::class)->findOneBy(['name' => $companyName]); if (!$company) { $company = new Company(); $company->setName($companyName); $this->entityManager->persist($company); // 这里完全不用存到数组了!EntityManager会帮你跟踪 } $contact = new Contact(); $contact->setCompany($company); $this->entityManager->persist($contact); // ... 处理Contact的其他字段 } // 最后统一提交到数据库 $this->entityManager->flush();
这个方案的好处是代码改动极小,而且完全避免了大数组的内存占用。不过如果你的行数特别多(比如几万行),所有实体还是会存在EntityManager的缓存里,可能还是会有内存压力——这时候可以试试下面的分批处理方案。
核心思路是把大批次的数据拆成小批量,每处理完一批就把EntityManager的缓存清空,释放内存。因为每批处理完会flush到数据库,后续再遇到相同公司时,直接从数据库查询即可。
代码示例:
$batchSize = 100; // 可以根据内存情况调整,比如500或1000 $rowCount = 0; foreach ($rows as $row) { $companyName = // 从当前行提取公司名称 $company = $this->entityManager()->getRepository(Company::class)->findOneBy(['name' => $companyName]); if (!$company) { $company = new Company(); $company->setName($companyName); $this->entityManager->persist($company); } $contact = new Contact(); $contact->setCompany($company); $this->entityManager->persist($contact); // ... 处理Contact的其他字段 $rowCount++; // 每处理完一批就提交并清空缓存 if ($rowCount % $batchSize === 0) { $this->entityManager->flush(); $this->entityManager->clear(); // 清空一级缓存,释放内存 } } // 处理最后一批剩余的数据 $this->entityManager->flush(); $this->entityManager->clear();
这个方案能有效控制内存占用,不管多少行数据,内存都只会保留当前批次的实体。唯一的小缺点是每批处理后,后续查询相同公司需要从数据库取,但对于批量导入来说,这个性能损耗完全可以接受。
如果你的数据量特别大,而且相同公司的重复率很高,可以开启Doctrine的二级缓存。二级缓存会把查询结果缓存到外部存储(比如Redis、Memcached),即使你清空了EntityManager的一级缓存,下次查询也能直接从二级缓存获取,不用再查数据库。
首先需要在Company实体上开启缓存注解:
/** * @Entity * @Cache(usage="READ_WRITE") */ class Company { // ... 你的实体字段和方法 }
然后在Doctrine的配置文件中配置二级缓存的驱动(比如Redis),开启后,查询Company时会自动走二级缓存,完全不用自己维护任何本地数组,内存占用极低。
内容的提问来源于stack exchange,提问作者Tibo

