Batch insert utility for Doctrine ORM
Since this package is hosted in a private GitLab repository, you'll need to add it to your project manually.
**** 1. Add the repository to your composer.json:
{
"repositories": [
{
"type": "vcs",
"url": ""
}
],
"require": {
"makxtr/doctrine-batch-insert": "dev-master"
}
}composer require makxtr/doctrine-batch-insertThe bundle automatically registers itself. All services are available through dependency injection:
// In your controller or service
public function __construct(
private BatchInsertServiceInterface $batchInsertService
) {}Your entities must implement the BatchInsertableInterface(only for light versions):
<?php
use makxtr\DoctrineBatchInsert\BatchInsertableInterface;
class User implements BatchInsertableInterface
{
private ?int $id = null;
private string $name;
private string $email;
private \DateTimeImmutable $createdAt;
public function __construct(string $name, string $email)
{
$this->name = $name;
$this->email = $email;
$this->createdAt = new \DateTimeImmutable();
}
public function getBatchInsertData(): array
{
return [
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->createdAt,
];
}
// getters and setters...
}<?php
namespace App\Controller;
use Doctrine\Common\Collections\ArrayCollection;
use makxtr\DoctrineBatchInsert\BatchInsertServiceInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
class DataImportController
{
public function __construct(
private BatchInsertServiceInterface $batchInsertService
) {}
public function importUsers(): JsonResponse
{
$users = new ArrayCollection([
new User('John Doe', 'john@example.com'),
new User('Jane Smith', 'jane@example.com'),
]);
$this->batchInsertService->lightBatchInsert($users);
return new JsonResponse(['status' => 'success']);
}
}Use when: Simple entities without relations, maximum performance needed, no need for inserted IDs.
$users = new ArrayCollection([
new User('John Doe', 'john@example.com'),
new User('Jane Smith', 'jane@example.com'),
]);
$batchInsertService->lightBatchInsert($users);Use when: Need data from inserted records (e.g., IDs), simple entities without complex relations.
// Return all fields
$results = $batchInsertService->lightBatchInsertWithResult($users);
// Return specific fields only
$results = $batchInsertService->lightBatchInsertWithResult($users, ['id', 'email']);
foreach ($results as $result) {
echo "Inserted user with ID: " . $result['id'] . "\n";
}Use when: Entities with relations, need conflict resolution strategies, require precise control.
use makxtr\DoctrineBatchInsert\DTO\Request\BatchInsertRequest;
$request = new BatchInsertRequest(
entityCollection: $users,
withRelations: true,
updateStrategy: $updateStrategy, // optional
conflictFields: ['email'],
updateFields: ['name'],
);
$insertedCollection = $batchInsertService->batchInsert($request);Use when: All features of batchInsert() + need insertion results.
$request = new BatchInsertRequest(
entityCollection: $users,
withRelations: true,
returnFields: ['id', 'name', 'email'],
);
$results = $batchInsertService->batchInsertWithResult($request);Standard INSERT without special duplicate handling.
use makxtr\DoctrineBatchInsert\UpdateStrategy\DefaultStrategy;
$request = new BatchInsertRequest(
entityCollection: $users,
updateStrategy: new DefaultStrategy()
);Ignores records that conflict with existing data.
use makxtr\DoctrineBatchInsert\UpdateStrategy\IgnoreUpdate;
$request = new BatchInsertRequest(
entityCollection: $users,
updateStrategy: new IgnoreUpdate(),
conflictFields: ['email'] // conflict detection fields
);Updates existing records when conflicts are detected.
use makxtr\DoctrineBatchInsert\UpdateStrategy\OnDuplicateKeyUpdate;
$request = new BatchInsertRequest(
entityCollection: $users,
updateStrategy: new OnDuplicateKeyUpdate(),
conflictFields: ['email'],
updateFields: ['name', 'updated_at'], // fields to update
mergeFields: ['login_count'] // fields to merge (sum/append)
);Completely replaces existing records with new data.
use makxtr\DoctrineBatchInsert\UpdateStrategy\Replace;
$request = new BatchInsertRequest(
entityCollection: $users,
updateStrategy: new Replace()
);class User implements BatchInsertableInterface
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
public function getBatchInsertData(): array
{
return [
'name' => $this->name,
'email' => $this->email,
];
// ID excluded - will be generated automatically
}
}use Symfony\Component\Uid\Uuid;
class Product implements BatchInsertableInterface
{
#[ORM\Id]
#[ORM\Column(type: 'uuid')]
private ?Uuid $id = null;
public function getBatchInsertData(): array
{
return [
'id' => $this->id, // UUID generated automatically if null
'name' => $this->name,
'price' => $this->price,
];
}
}The library automatically handles ManyToOne and OneToOne relationships:
class Order implements BatchInsertableInterface
{
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
private User $user;
#[ORM\ManyToOne(targetEntity: Product::class)]
#[ORM\JoinColumn(name: 'product_id', referencedColumnName: 'id')]
private Product $product;
public function getBatchInsertData(): array
{
return [
'quantity' => $this->quantity,
'price' => $this->price,
'created_at' => $this->createdAt,
// user_id and product_id extracted automatically from related objects
];
}
}
$orders = new ArrayCollection([
new Order($user1, $product1, 2, 100.00),
new Order($user2, $product2, 1, 50.00),
]);
$request = new BatchInsertRequest(
entityCollection: $orders,
withRelations: true
);
$batchInsertService->batchInsert($request);$request = new BatchInsertRequest(
entityCollection: $entities, // Collection of entities to insert
returnFields: ['id', 'name'], // Fields to return in result
updateFields: ['name', 'email'], // Fields to update on conflict
mergeFields: ['view_count'], // Fields to merge (sum/append values)
conflictFields: ['email'], // Fields for conflict detection
withRelations: true, // Process related entities
updateStrategy: new OnDuplicateKeyUpdate() // Conflict resolution strategy
);public function createUsers(array $userData): void
{
foreach ($userData as $data) {
$user = new User($data['name'], $data['email']);
$this->entityManager->persist($user);
}
$this->entityManager->flush();
}public function createUsers(array $userData): void
{
$users = new ArrayCollection();
foreach ($userData as $data) {
$users->add(new User($data['name'], $data['email']));
}
$this->batchInsertService->lightBatchInsert($users);
}Change entities for you project
When adding/removing a field from an entity you don't forget to update the getBatchInsertData method
class BatchInsertableTest extends KernelTestCase
{
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::$kernel->getContainer()->get('doctrine.orm.entity_manager');
}
#[DataProvider('getEntitiesDataProvider')]
public function testBatchInsertableEntities(BatchInsertableInterface $entity): void
{
$classMetadataFactory = $this->entityManager->getMetadataFactory();
$classMetadata = $classMetadataFactory->getMetadataFor(get_class($entity));
$expectedFields = $this->obtainFields($classMetadata);
sort($expectedFields);
$providedFields = array_keys($entity->getBatchInsertData());
sort($providedFields);
$this->assertEquals($expectedFields, $providedFields);
}
public static function getEntitiesDataProvider(): array
{
$message = (new Message(
'test',
1,
1,
1,
null,
null
))
->setId(1);
return [
'notification channel' => [
(new RecipientNotificationChannel(1, 1))
->setMessageId(1)
->setVersionTemplateId(1)
->setCategory(1),
],
'personal account channel' => [
(new RecipientPersonalAccountChannel(1, 1))
->setMessageId(1)
->setVersionTemplateId(1),
],
'email channel' => [
(new RecipientEmailChannel(1, 1))
->setMessageId(1)
->setVersionTemplateId(1),
],
'push channel' => [
(new RecipientPushChannel(1, 1))
->setMessageId(1)
->setVersionTemplateId(1),
],
'recipient email' => [
(new RecipientEmail(1, 'test@gmail.com'))
->setMessage($message)
->setPartnerSupportEmail('test@gmail.com')
->setLanguage('ua')
->setVariable(['test' => 1]),
],
'recipient push' => [
(new RecipientPush('UA', 1))->setMessage($message),
],
];
}
private function obtainFields(ClassMetadata $classMetadata): array
{
$directFields = array_keys($classMetadata->fieldNames);
if ($classMetadata->usesIdGenerator()) {
unset($directFields[0]);
}
$relationFields = [];
foreach ($classMetadata->associationMappings as $associationMapping) {
if ($this->shouldSkipAssociation($associationMapping)) {
continue;
}
$sourceToTargetKeyColumn = array_key_first($associationMapping['sourceToTargetKeyColumns']);
$relationFields[] = $sourceToTargetKeyColumn;
}
return array_merge($directFields, $relationFields);
}
private function shouldSkipAssociation(array $associationMapping): bool
{
$allowedTypes = [
ClassMetadataInfo::MANY_TO_ONE,
ClassMetadataInfo::ONE_TO_ONE,
];
return
!in_array($associationMapping['type'], $allowedTypes, true) ||
!$associationMapping['isOwningSide'];
}
}The Doctrine Batch Insert Bundle provides a powerful and flexible tool for bulk database operations. Choose the appropriate method based on your needs:
- For simple cases, use
lightBatchInsert() - For complex scenarios with relations, use
batchInsert() - When you need results, use methods with
WithResultsuffix
This achieves optimal balance between performance and functionality in your application.