---
title: Writing Data With the Data Abstraction Layer | Shopware Community Hub
description: >-
  Learn how to create, update, and delete custom entity data using Shopware’s
  Data Abstraction Layer.
canonical_url: 'https://hub.shopware.com/learn/unit/writing-data-with-the-dal'
---

# Writing Data With the Data Abstraction Layer

<LearningObjectives>

- Understand how to create, update, and delete data in custom entities using Shopware's DAL.
- Learn to use the repository's upsert method for both creating and updating records.
- Understand bulk operations for handling multiple records effectively.
- Understand validation, logging, and exception handling when writing data.
- Use the Context object to ensure data operations are performed in the correct scope
- Apply best practices for data integrity and performance when writing data via the DAL.
  
</LearningObjectives>

# Writing Data to Custom Entities

Now that you've learned how to read data from your custom entities, let's explore how to write data to them.

In this learning unit, we'll dive into the practical aspects of creating, updating, and deleting product notes using Shopware's Data Abstraction Layer.

Writing data in Shopware is fundamentally different from reading it. While reading focuses on building queries and criteria, writing involves preparing data structures and using the repository's write methods. This approach ensures data consistency, validation, and proper event handling throughout the system.

<Callout title="Note" type="info">

The examples in this learning unit focus on **DAL write operations** and show them in a service class.

The AcademyProductNotes plugin is **not a fully wired feature** (e.g., no controllers).

The goal is to teach the **core patterns** you can reuse in your own plugins.

</Callout>

## Understanding Data Writing in Shopware

When you write data to entities in Shopware, you're not just inserting records into a database.

Instead, you pass structured data to repository write methods such as `create()`, `update()`, `upsert()`, or `delete()`. This ensures that all write operations go through Shopware's Data Abstraction Layer.

As a result, Shopware can:

- **Validate your data** based on the entity definition (e.g., required fields).
- **Triggers internal reactions** (e.g., events), so that other parts of the system can react to changes.
- **Enforce data integrity** through foreign key constraints and relationships.
- **Keep the system consistent**, by running follow-up processes, such as event listeners or indexing, when data changes.
- **Apply the current context**, so permissions, language, and scope are respected during write operations.

The key to successful data writing is understanding how to structure your data and when to use different write methods. Let's explore this step by step.

## Step 1: Creating New Product Notes

Creating new records is the foundation of data writing. In Shopware, you'll typically use the `upsert` method, which can handle both creation and updates. Let's start with the basics.

### Basic Product Note Creation

```php
<?php declare(strict_types=1);

namespace AcademyProductNotes\Service;

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;

class ProductNoteService
{
    public function __construct(
        private readonly EntityRepository $productNoteRepository,
        private readonly EntityRepository $productRepository,
        private readonly LoggerInterface  $logger,
    ) {}

    public function createProductNote(string $productId, string $note, Context $context): ?string
    {
        $data = [
            [
                'productId' => $productId,
                'productVersionId' => Defaults::LIVE_VERSION,
                'note' => $note
            ]
        ];

        $result = $this->productNoteRepository->upsert($data, $context);
        
        // Return the ID of the newly created product note
        return $result->getEventByEntityName(ProductNoteDefinition::ENTITY_NAME)?->getIds()[0] ?? null;
    }
}
```

Let me explain what's happening here. The `createProductNote` method takes the essential information needed to create a product note: the product ID and the note content itself. The DAL automatically handles the `createdAt` and `updatedAt` fields, so you don't need to provide them.

The `upsert` method is particularly powerful because it can handle both creating new records and updating existing ones. When you don't provide an ID in your data array, Shopware automatically generates a new UUID for the record. This is why we don't need to specify an `id` field in our data array.

The `productVersionId` is part of the product reference because products are [version-aware entities](https://developer.shopware.com/docs/concepts/framework/data-abstraction-layer.html#versioning) in Shopware.

A specific product version is identified by two values:

- `productId`: Which product is this?
- `productVersionId`: Which version of that product is this?

You already defined this relationship in the custom entity. The `ProductNote` table stores both `product_id` and `product_version_id`, and the database foreign key points to `product(id, version_id)`.

In this example, the note should be linked to the product version currently used by the live shop. Therefore, the payload uses:

```php
'productVersionId' => Defaults::LIVE_VERSION,
```

`Defaults::LIVE_VERSION` is Shopware's fixed identifier for the live version. This line does not create a new product version or modify the product. It only completes the reference from the product note to a specific product version.

In plain language, the payload says: "Create this note for this product and link it to its live version."

If your code worked inside another version-aware workflow, you would use that workflow's product version ID instead. For this learning example, `Defaults::LIVE_VERSION` is the correct value because the note is created for the product that is currently live.

<Callout title="What Happens if productVersionId Is Missing?" type="info">

In this simple create example, Shopware normally falls back to the live version if you did not provide `productVersionId` yourself, or if you explicitly set it to `null`.

This happens because Shopware's `ReferenceVersionField` handling resolves the missing version reference during the DAL write process. The database column still does not store `NULL`; the reference is completed with the live version.

This fallback is useful to understand, but explicit code is easier to read in a learning example. By using `Defaults::LIVE_VERSION`, the payload clearly shows that the note belongs to the live product version.

</Callout>

You may notice, that the `$data` is an array, and we wrapped the first parameter also in an array. This is because the `upsert` method expects an array of data structures (array), so array of write payloads (2d array). That means: even for one record, you still pass an array with one "array-item".

<Callout title="Note" type="info">

If you want to dive deeper into of what methods the `EntityRepository` class provide, feel free to check the [official implementation](https://github.com/shopware/shopware/blob/trunk/src/Core/Framework/DataAbstractionLayer/EntityRepository.php), the first parameter is defined as `array<array<string, mixed|null>`.

</Callout>

So you can also create your 2d array like this:

```php
$data = [
    [
        'productId' => $productId,
        'userId' => $userId,
        'note' => $note,
    ],
     [
        'productId' => $productId,
        'userId' => $userId,
        'note' => $note,
    ],
    // ... additional 1d array seperated by comma
    
];
```

And call the upsert method like this:

```php
$result = $this->productNoteRepository->upsert($data, $context);
```

### Understanding the Upsert Method

Before continuing, let's quickly check if you remember what you learned about the `upsert` method.

<ArticleMultipleQuestionnaire>
  <ArticleQuestionnaire>
    <ArticleQuestionnaireQuestion>What is the difference between update and upsert? (Multiple answers possible.)</ArticleQuestionnaireQuestion>
    <ArticleQuestionnaireAnswer correct>The update method only runs when the entity already exits.</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer>The upsert and the update methods are the same.</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer correct>The upsert method creates a new entity if it does not exist yet, or updates it if it does.</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer>The upsert method only works when the entity already exits.</ArticleQuestionnaireAnswer>
  </ArticleQuestionnaire>
</ArticleMultipleQuestionnaire>

The `upsert` method works like `update` **or** `insert`. This means:

- If an `id` is **missing**, the DAL creates a new record and generates a new UUID for it.
- If an `id` is **provided** and the record exists, the DAL updates the existing record.
- If an `id` is **provided** but the record does not exist, the DAL creates a new record with that ID.
- **Bulk operations**: You can pass multiple data arrays to handle many records at once.
- **Automatic validation**: Shopware validates all data against your entity definition before writing.

This approach is much more flexible than having separate `create` and `update` methods, and it's the recommended way to handle data writing in Shopware.

## Step 2: Updating Existing Product Notes

Updating records follows a similar pattern to creation, but you need to provide the ID of the record you want to modify.

### Updating a Product Note

```php
public function updateProductNoteContent(string $id, string $note, Context $context): bool
{
    try {
        $data = [
            'id' => $id,
            'note' => $note
        ];

        $result = $this->productNoteRepository->upsert([$data], $context);
        $resultIds = $result->getEventByEntityName(ProductNoteDefinition::ENTITY_NAME)?->getIds() ?? [];

        // Check if the update was successful
        return in_array($id, $resultIds, true);
    } catch (Throwable $t) {
        // Log the error for debugging
        $this->logger->error(
            'Failed to update product note content',
            [
                'productNoteId' => $id,
                'exception' => $t,
                'exceptionCode' => $t->getCode(),
                'exceptionMessage' => $t->getMessage(),
            ]
        );
        return false;

        // In a real application, you might want to throw a custom exception instead of returning false
        // throw $t;
    }
}
```

Notice the key difference here: we're including the `id` field in our data array. This tells Shopware that we want to update an existing record rather than create a new one. The `upsert` method will then:

1. Look for a record with the specified ID
2. Update the fields we've provided (note and updatedAt)
3. Leave other fields unchanged
4. Return the IDs of all records that were affected

### How Do We Know If an Update Was Successful?

The methods `update()`, `upsert()`, and `delete()` return an `EntityWrittenContainerEvent` object. We can fetch the `EntityWrittenEvent` for our entity and read the affected IDs:

- `getEventByEntityName(...)` returns the `EntityWrittenEvent` object for the specified entity name or `null`.
- `getIds()` returns the IDs affected by the operation.

That's why we use `?? []`: If the event is `null`, we fall back to an empty array so `in_array` always receives an array and doesn't break.

### Partial Updates and Data Integrity

One of the beautiful aspects of Shopware's approach is that you only need to specify the fields you want to change. All other fields remain untouched, which helps maintain data integrity. For example, if you only want to update the note content, you could simplify the update method:

```php
public function updateNoteContent(string $id, string $note, Context $context): bool
{
    $data = [
        'id' => $id,
        'note' => $note,
    ];

    $result = $this->productNoteRepository->upsert([$data], $context);
    return in_array($id, $result->getEventByEntityName(ProductNoteDefinition::ENTITY_NAME)?->getIds() ?? []);
}
```

This approach is much safer than trying to update all fields manually, as it reduces the risk of accidentally overwriting data you didn't intend to change.

## Step 3: Bulk Operations for Multiple Records

One of the most powerful features of Shopware's DAL is the ability to handle multiple records in a single operation. This is especially useful when you need to create or update many product notes at once.

### Creating Multiple Product Notes

```php
public function createMultipleProductNotes(array $notesData, Context $context): array
{
    $data = [];

    foreach ($notesData as $noteData) {
        $data[] = [
            'productId' => $noteData['productId'],
            'productVersionId' => Defaults::LIVE_VERSION,
            'note' => $noteData['note'],
        ];
    }

    $result = $this->productNoteRepository->upsert($data, $context);

    return $result->getEventByEntityName(ProductNoteDefinition::ENTITY_NAME)?->getIds() ?? [];
}
```

This method demonstrates how to efficiently create multiple product notes in a single database operation. Instead of making individual calls to the repository for each note, we batch them together. This approach has several advantages:

- **Performance**: Fewer database round trips mean faster execution
- **Transaction safety**: All operations succeed or fail together
- **Consistency**: Better data integrity across multiple records
- **Scalability**: Can handle hundreds or thousands of records efficiently

### Bulk Updates with Different Operations

Sometimes you need to perform different operations on different records in the same batch. The `upsert` method handles this seamlessly:

```php
public function bulkUpdateProductNotes(array $updates, Context $context): array
{
    $data = [];

    foreach ($updates as $update) {
        if (isset($update['id'])) {
            // This is an update operation
            $data[] = [
                'id' => $update['id'],
                'note' => $update['note'],
                'updatedAt' => new DateTimeImmutable(),
            ];
        } else {
            // This is a create operation
            $data[] = [
                'productId' => $update['productId'],
                'productVersionId' => Defaults::LIVE_VERSION,
                'note' => $update['note'],
            ];
        }
    }

    $result = $this->productNoteRepository->upsert($data, $context);
    $resultEntityIds = $result->getEventByEntityName(ProductNoteDefinition::ENTITY_NAME)?->getIds() ?? [];

    return [
        'created' => array_filter($resultEntityIds, fn($id) => !in_array($id, array_column($updates, 'id'))),
        'updated' => array_filter($resultEntityIds, fn($id) => in_array($id, array_column($updates, 'id'))),
    ];
}
```

This method shows how you can mix create and update operations in the same batch. It's particularly useful when you're importing data from external sources or when you need to synchronize data between different systems.

## Step 4: Deleting Product Notes

Deleting data is just as important as creating and updating it. Shopware provides several methods for removing records, each with different use cases.

### Deleting a Single Product Note

```php
public function deleteProductNote(string $id, Context $context): bool
{
    try {
        $result = $this->productNoteRepository->delete([['id' => $id]], $context);
        $resultIds = $result->getEventByEntityName(ProductNoteDefinition::ENTITY_NAME)?->getIds() ?? [];

        // Check if the deletion was successful
        return in_array($id, $resultIds, true);
    } catch (Throwable $t) {
        // Handle deletion errors appropriately
        return false;
    }
}
```

The `delete` method works similarly to `upsert` - it takes an array of data structures, but in this case, you only need to provide the ID of the record you want to remove. Shopware will handle the rest, including:

- Checking if the record exists
- Removing any associated data (depending on your foreign key constraints)
- Triggering appropriate deletion events
- Updating any caches that might reference the deleted record

### Bulk Deletion for Multiple Records

When you need to remove multiple records at once, bulk deletion is the way to go:

```php
public function deleteMultipleProductNotes(array $ids, Context $context): array
{
    $data = array_map(fn($id) => ['id' => $id], $ids);

    $result = $this->productNoteRepository->delete($data, $context);

    return $result->getEventByEntityName(ProductNoteDefinition::ENTITY_NAME)?->getIds() ?? [];
}
```

This approach is much more efficient than deleting records one by one, especially when dealing with larger datasets. It's also safer because all deletions happen within a single transaction.

### Deleting vs. Deactivating Data

Before deleting data, you should carefully consider whether the record should be **removed permanently** or simply **marked as inactive**.

**Delete data when:**

- The data has **no long-term business value**
- There is **no audit or legal requirement** to keep it
- The data is purely **technical or temporary**

**Prefer an active/inactive flag when:**

- The data is part of **business history**
- You need **audit trails** or change tracking
- Other entities reference this record
- The data may be **reactivated later**

<Callout title="Best Practice" type="info">

Deletion is **not** reversible. If you are unsure whether data might be needed later, it is better to deactivate it rather than delete it permanently!

</Callout>

## Step 5: Error Handling and Validation

Writing data can fail for many reasons: validation errors, database constraints, network issues, or permission problems. Proper error handling is crucial for building robust applications.

### Comprehensive Error Handling

```php
public function createProductNoteWithValidation(string $productId, string $note, Context $context): array
{
    // Basic validation
    if (empty($note)) {
        $this->logger->error(
            'Note content cannot be empty',
            [
                'productId' => $productId,
                'note' => $note,
            ]
        );
        throw new InvalidArgumentException('Note content cannot be empty');
    }

    if (strlen($note) > 1000) {
        $this->logger->error(
            'Note content is too long (max 1000 characters)',
            [
                'productId' => $productId,
                'note' => $note,
            ]
        );
        throw new InvalidArgumentException('Note content is too long (max 1000 characters)');
    }

    try {
        $data = [
            'productId' => $productId,
            'productVersionId' => Defaults::LIVE_VERSION,
            'note' => trim($note),
        ];

        $result = $this->productNoteRepository->upsert([$data], $context);

        return [
            'success' => true,
            'id' => $result->getEventByEntityName(ProductNoteDefinition::ENTITY_NAME)?->getIds()[0],
            'message' => 'Product note created successfully'
        ];
    } catch (Throwable $t) {
        $this->logger->error(
            'Failed to create product note',
            [
                'productId' => $productId,
                'note' => $note,
                'exception' => $t,
                'errorCode' => $t->getCode(),
                'errorMessage' => $t->getMessage(),
            ]
        );
        throw $t;
    }
}
```

This method demonstrates several important error handling concepts:

- **Input validation:** Checking data before attempting to write it.
- **Logging:** Store relevant details in logs for debugging and monitoring.
- **Exceptions:** Let the calling code (e.g., a controller) decide how to handle and respond to errors.

### Understanding Shopware Exceptions

Shopware throws different types of exceptions depending on what goes wrong:

- **InconsistentCriteriaIdsException**: Usually means you're trying to reference entities that don't exist
- **ConstraintViolationException**: Data doesn't meet the validation requirements defined in your entity
- **AccessDeniedException**: The current user doesn't have permission to perform the operation
- **EntityNotFoundException**: The entity you're trying to update or delete doesn't exist

Understanding these exceptions helps you provide better error handling and user feedback.

## Step 6: Practical Examples from Your AcademyProductNotes Plugin

Let's look at some real-world scenarios you might encounter when building your product notes system.

### Adding a Note to a Product

```php
public function addNoteToProduct(string $productId, string $note, Context $context): ?array
{
    // First, check if the product exists and is active
    $productCriteria = new Criteria([$productId]);
    $productCriteria->addFilter(new EqualsFilter('active', true));

    $product = $this->productRepository->search($productCriteria, $context)->first();
    if (null === $product) {
        return null;
    }

    // Create the product note
    return $this->createProductNoteWithValidation($productId, $note, $context);
}
```

This method shows how you might implement a complete workflow for adding notes to products. It includes:

- **Pre-validation**: Checking that the product and user exist before creating the note
- **Business logic**: Ensuring the product is active before allowing notes
- **Reuse**: Calling the validation method we created earlier
- **Comprehensive feedback**: Returning detailed information about the operation's success or failure

### Updating Notes with Optional History Tracking

```php
public function updateNoteWithHistory(string $id, string $newNote, Context $context): ?string
{
    // First, get the current note to preserve the previous value
    $currentNote = $this->getProductNoteById($id, $context);
    if (null === $currentNote) {
        return null;
    }

    // Conceptual example:
    // If your plugin has a separate note_history entity, you could prepare
    // a history payload before updating the main note.
    $historyData = [
        'noteId' => $id,
        'oldNote' => $currentNote->getNote(),
        'newNote' => $newNote,
        'changedAt' => new DateTimeImmutable(),
    ];

    // Update the main note as part of the note-with-history workflow.
    // The DAL handles updatedAt automatically, so we only pass the fields we want to change.
    $updateData = [
        'id' => $id,
        'note' => $newNote,
    ];

    $result = $this->productNoteRepository->upsert([$updateData], $context);

    // Conceptual example only:
    // If your plugin defines a note_history entity and injects its repository,
    // you could write the history record here.
    //
    // $this->noteHistoryRepository->upsert([$historyData], $context);

    return $result->getEventByEntityName(ProductNoteDefinition::ENTITY_NAME)?->getIds()[0] ?? null;

}
```

This example demonstrates more advanced concepts like:

- **Data preservation**: Reading the current value before updating it, so the previous state can be preserved if your project needs history tracking.
- **Conceptual extension**: Understanding how a separate history entity could be added later.
- **Consistency awareness**: Understanding that related writes, such as updating a note and writing a history record, should be handled together according to your project's requirements.
- **Business rules**: Implementing custom logic for your specific use case
- **Extensibility**: Planning for future features like change history

## Best Practices

As you work with writing data in Shopware, keep these important principles in mind:

1. **Always use the repository methods**: Never try to write directly to the database
2. **Validate input data**: Check data before attempting to write it
3. **Handle errors gracefully**: Provide meaningful feedback when operations fail
4. **Use bulk operations**: Group related operations together for better performance
5. **Consider transactions**: Ensure data consistency across multiple operations
6. **Respect the Context**: Always pass the appropriate context for proper permission handling
7. **Think about events**: Remember that your write operations will trigger system events

## Summary

Great! By completing this learning unit, you've learned:

- Write data to custom entities using the DAL.
- Create, update, and delete entity records via repository write methods.
- Use the `upsert` method for both single and bulk write operations.
- Structure payloads for write operations correctly for custom entities.
- Use the `Context` object to ensure correct permissions and scope.

With this knowledge, you have a solid foundation for implementing reliable and maintainable data-writing workflows in your Shopware applications.
