Doctrine2与CodeIgniter外键插入

7

我有以下数据库架构 -

数据库架构

现在,部门、年份和分区表已经填充了信息。

现在我需要插入学生数据。 学生数据将从xls文件导入(导入和解析部分已完成)。 如您在模式中所见,student_data表的列引用year_iddepartment_didivision_id。 因此,在插入时,我需要它们的ID字段,因为xls具有相应的名称值。

因此,根据每个学生的列值,我必须获取相应的ID。 这样,插入一个记录到学生表中需要3个查询。 就像这样 -

forloop(...):
     $studentData = new Entities\StudentData();

    $year =  $this->em->getRepository("Entities\Year")->findBy(array('year_name' => $this->year[$i]));
    $department =  $this->em->getRepository("Entities\Department")->findBy(array('department_name' => $this->branch[$i]));
    $division =  $this->em->getRepository("Entities\Division")->findBy(array('division_name'=>$this->division[$i]));

    $studentData->setYear($year[0]);
    $studentData->setDepartment($department[0]);
    $studentData->setDivision($division[0]);

    //other data
    .
    .
    .
    .
    .
    $this->em->persist($studentData);

endforloop();   

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

您可以看到,在每个部门、年份和分区的循环中,我都需要获取ID。假设我正在导入100名学生名单,那么它最终会运行300个查询,只是为了获取这3个ID字段。

在插入数据时,我能否直接从它们的名称中获取年份、部门和分区的ID?我对Doctrine还不熟悉,不知道该怎么做。


更新 如果问题不清楚,请让我知道。我可以用更多的细节来更新它或重新组织它。

2个回答

4

优化

您可以在不使用Doctrine的结果缓存的情况下优化您的过程:

首先创建一个年份到其ID的映射,如下所示:

$yearsMap = array();

$q = $em->createQuery('SELECT y.id, y.year_name FROM Entities\Year y');

foreach ($q->getScalarResult() as $row) {
    $yearsMap[$row['year_name']] = $row['id'];
}

同时创建一个部门到其ID的映射表,以及一个分区到其ID的映射表。这将导致3个轻量级查询。最好的放置代码的位置是在(自定义)仓库中。
接下来,您可以运行循环,但是像这样“获取”实际实体:
$year       = $this->em->getReference('Entities\Year', $yearsMap[$this->year[$i]]);
$department = $this->em->getReference('Entities\Department', $departmentsMap[$this->branch[$i]]);
$division   = $this->em->getReference('Entities\Division', $divisionsMap[$this->division[$i]]);

我说“获取”,因为getReference()实际上创建了一个代理(除非它已经被实体管理器加载,但在这种情况下可能不是)。该代理尚未加载,因此此处不执行任何查询。

您的其余代码不需要更改。

现在调用flush()时,Doctrine将仅加载每个不同的年份/部门/分部一次。这可能仍然导致一些查询,取决于使用多少个不同的年份/部门/分部。因此,如果所有100名学生使用不同的年份/部门/分部,则最终会产生403个查询(3个用于映射,300个用于加载代理,100个用于插入学生)。但是,如果所有100名学生使用相同的年份/部门/分部,则最终只会产生106个查询(3个用于映射,3个用于加载代理,100个用于插入学生)。

另一种优化方式

另一种方法是使用您收集的名称来获取所需的所有实体:

$q = $em->createQuery('SELECT y FROM Entities\Year y INDEX BY y.year_name WHERE y.year_name IN(:years)');
$q->setParameter('years', $yearNames);

$yearsMap = $q->getResult();

现在你只需要一个查询就可以获得所有所需的年份实体。部门和分区也可以采用相同的方法。

还要注意DQL语句中的INDEX BY:这将确保你得到一个以year_name为键,实体为值的数组。你可以直接在循环中使用它,如下所示:

$year       = $yearsMap[$this->year[$i]];
$department = $departmentsMap[$this->branch[$i]];
$division   = $divisionsMap[$this->division[$i]];

The end result for 100 students will always be 103 queries (3 for the maps, 100 for inserting students).
Cache 当您需要经常运行此循环并且它会对数据库造成压力时,最好使用Doctrine的result cache。但是需要注意一些事项: getReference()尚不支持结果缓存,而且结果缓存不会自动使用。因此,建议您在存储库中放置类似以下内容的代码:
public function findOneYearByName($name)
{
    $q = $em->createQuery('SELECT y FROM Entities\Year y WHERE y.year_name = :year');
    $q->setParameter('year', $name);
    $q->useResultCache(true);

    return $q->getSingleResult();
}

您可能想要配置结果缓存,请参见文档

另一个需要注意的是,结果缓存将缓存从数据库中获取的结果,在其被填充之前。因此,即使使用结果缓存,实际的实体也会每次都被填充。因此,我仍然建议使用映射,但实现方式略有不同:

$yearsMap       = array();
$departmentsMap = array();
$divisionsMap   = array();

forloop (...):
    if (!isset($yearsMap[$this->year[$i]])) {
        $yearsMap[$this->year[$i]] = $this->em->getRepository('Entities\Year')->findOneYearByName($this->year[$i]);
    }

    if (!isset($departmentsMap[$this->branch[$i]])) {
        $departmentsMap[$this->branch[$i]] = $this->em->getRepository('Entities\Department')->findOneDepartmentByName($this->branch[$i]);
    }

    if (!isset($divisionsMap[$this->division[$i]])) {
        $divisionsMap[$this->division[$i]] = $this->em->getRepository('Entities\Division')->findOneDivisionByName($this->division[$i]);
    }

    $year       = $yearsMap[$this->year[$i]];
    $department = $departmentsMap[$this->branch[$i]];
    $division   = $divisionsMap[$this->division[$i]];

这样可以确保每个不同的年份/部门/分区只会被一次注入水分。
PS:对于“另一种优化方式”,使用结果缓存效果不佳,因为年份/部门/分区名称在每次运行循环时可能是不同的。随着名称的变化,查询也会发生变化,无法使用缓存的结果。
DBAL
问:我可以在插入数据时直接根据名称获取年份、部门和分区的ID吗?
答:您可以这样做,但您只能使用DBAL而不是ORM。基本上是这样的:
$connection = $em->getConnection();
$statement  = $conn->executeQuery('insert query', array('parameter1', 'etc'));
$statement->execute();

我认为这种方法并不会更加高效,因为MySQL(或者你使用的其他供应商)仍然会为每个插入执行那三个(子)查询,只是这些查询不会“通过网络传输”。而且你也无法从ORM中获得任何帮助,例如管理关联等。
不过,你可以在这里找到有关该主题的所有内容。

3

你检查过它是否运行了300个查询吗?因为除非所有学生的年份、系别和部门都是完全不同的,否则它绝对不应该运行300个查询,这似乎非常不可能。即使是这样,除非进行其他优化,否则300个查询至少也是必需的,无论是否使用Doctrine。

好消息是,Doctrine不仅仅是访问对象的花哨方式——它是一个完整的数据库抽象层,提供许多其他服务,例如一个完整的实体缓存。以下一行:

$year =  $this->em->getRepository("Entities\Year")->findBy(array('year_name' => $this->year[$i]));

对于一个给定的年份,执行最多1个查询 - 之后结果将完全被存储和初始化在Doctrine的内部缓存实体管理器中。这是假设你使用的是默认启用的MemoryCache,如果你没有指定其他内容,它只在单个请求期间进行缓存。如果您安装了APC、Memcache、Memcached甚至是FilesystemCache(选择其中一个!),结果可能会在多个请求期间缓存。

因此,简而言之,您正在想象一个不存在的问题,或者可以通过几个简单配置调用轻松解决。除非我们谈论的是所有年份、部门和分支机构都是唯一的假设情况 - 那么您确实正在触发300个查询。然而,在这种情况下,并不是Doctrine的问题 - 它只是执行您要求的操作,单独检索300个唯一对象。在这种情况下,没有人阻止您自己编写一些智能代码来处理Doctrine,例如:

// Build cache before loop
$years = [];
foreach($this->em->getRepository("Entities\Year")->findAll() as $year)
  $years[$year->getYearName()] = $year;

// Now loop much faster because everything's already indexed
forloop(...) :
  $studentData = new Entities\StudentData();
  $studentData->setYear($years[$this->year[$i]]);
endforloop;

突然之间,您只有一个“昂贵”的查询,而不是100个稍微便宜一些的查询。Doctrine是一个方便的工具,使得许多与数据库相关的编码变得更加容易和有结构性,它并不禁止像这样聪明的面向性能的编码方式。最终,您仍然是编码人员,Doctrine只是您可以随意使用的工具箱中的一个工具。


那个缓存的东西真是聪明而且最简单的方式。(撞墙中)我太傻了,没想到这一点。感谢那些有用的链接,即使我已经访问过它们,但留下了关键点。我对整体缓存不太擅长。我的公司生产服务器没有APC或memcache。我在服务器上使用eAccelator并进行了一些研究,发现它与doctrine不兼容。仍然不确定。但我正在我的Amazon EC2个人测试服务器上测试doctine和APC,并请求公司也更改他们的服务器。我发现使用APC是最快的方式。 - SachinGutte
感谢提供的链接和有用的建议。我仍在寻找最佳答案。 :) 因此,我会暂时不提供赏金。这并不是关于赏金或其他什么,而是我正在寻找可能的最佳答案,即使是为了未来的访问者。 - SachinGutte
嗯,我不确定您需要什么更多的信息 :) 我很乐意详细说明可能需要澄清的事情。 - Niels Keurentjes
啊..如果不麻烦的话,我想了解一下数据库查询及其缓存。我的意思是,Doctrine是否维护每个查询或类似查询的缓存?如果我实例化一个“Entity”的对象,Doctrine是否会查找缓存?如果有对多个带有外键的表的引用,如果我发出一个查询,Doctrine是否会获取所有这些引用?我知道这太多了,但这些只是我拥有的疑问(因为这对我来说是新的),但即使这些问题没有得到回答,随着我更多地使用它,我也会找到答案。所以如果可能的话,我想知道您对此的了解。 :) - SachinGutte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接