---
title: Reading Data With the Data Abstraction Layer | Shopware Community Hub
description: >-
  Learn how to read data from custom entities using Shopware’s Data Abstraction
  Layer (DAL).
canonical_url: 'https://hub.shopware.com/learn/unit/reading-data-with-the-dal'
---

# Reading Data With the Data Abstraction Layer

<LearningObjectives>

- Understand how to read data from custom entities using Shopware's DAL.
- Use the Criteria API for building complex database queries.
- Learn to use filters, sorting, pagination, and associations for efficient data retrieval.
- Use repositories to perform read operations on custom entities.
- Apply best practices for efficient data querying in Shopware.
  
</LearningObjectives>

# Reading Data From Custom Entities

Now that you've created your custom `ProductNote` entity, let's learn how to read and retrieve data from it efficiently. In this learning unit, we'll explore the various ways to query your custom entities using Shopware's powerful Data Abstraction Layer.

## Understanding Data Reading in Shopware

Reading data in Shopware is primarily done through **repositories** and the **Criteria API**. This approach provides:

- **Type safety** - Compile-time checking of your queries
- **Performance optimization** – Automatic query optimization and caching
- **Flexibility** – Complex queries with filters, sorting, and associations
- **Consistency** – Same API for all entities across the system

## The Repository Pattern

Every entity in Shopware has a corresponding repository that handles all data operations. For our `ProductNote` entity, we'll use the `ProductNoteRepository` to read data.

## Step 1: Basic Data Reading

Let's start with simple data retrieval operations.

### Reading All Product Notes

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

namespace AcademyProductNotes\Service;

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Swag\AcademyProductNotes\Content\ProductNote\ProductNoteCollection;

class ProductNoteService
{
    public function __construct(
        private readonly EntityRepository $productNoteRepository
    ) {}

    public function getAllProductNotes(Context $context): ProductNoteCollection
    {
        $criteria = new Criteria();
        
        return $this->productNoteRepository->search($criteria, $context)->getEntities();
    }
}
```

The `getAllProductNotes` method creates a new `Criteria` object without any filters, which means it will fetch all records from the `ProductNote` entity. It then calls the `search` method on the repository, passing in the criteria and the current Shopware `Context`. The result is a `ProductNoteCollection` containing all product notes in the system.

A **collection** in Shopware is an object that contains multiple entities of the same type. For example, when you fetch all product notes, you receive a `ProductNoteCollection`. This collection behaves similarly to an array, but provides additional methods for working with the entities it contains.

You can:

- **Iterate** over the collection using a `foreach` loop:

  ```php
  /** @var ProductNoteEntity $note */
  foreach ($productNoteCollection as $note) {
      // Access properties, e.g. $note->getNote()
  }
  ```

- **Access the first or last entity**:

  ```php
  $firstNote = $productNoteCollection->first();
  $lastNote = $productNoteCollection->last();
  ```

- **Count the number of entities**:

  ```php
  $count = $productNoteCollection->count();
  ```

- **Convert to an array**:

  ```php
  $notesArray = $productNoteCollection->getElements();
  ```

Collections provide a convenient and type-safe way to work with sets of entities returned from the repository.

### Reading a Single Product Note by ID

```php
public function getProductNoteById(string $id, Context $context): ?ProductNoteEntity
{
    $criteria = new Criteria([$id]);
    
    $result = $this->productNoteRepository->search($criteria, $context);
    
    return $result->first();
}
```

The `$id` parameter is passed to the `getProductNoteById` method when it is called. This value should be the unique identifier (UUID) of the product note you want to retrieve. Inside the method, the `$id` is used to create a new `Criteria` object that filters for the entity with that specific ID. The repository then searches for a product note matching this ID and returns the first result, or `null` if none is found.

## Step 2: Using Filters

Filters allow you to narrow down your search results based on specific criteria.

### Filtering by Product ID

```php
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;

public function getProductNotesByProduct(string $productId, Context $context): ProductNoteCollection
{
    $criteria = new Criteria();
    $criteria->addFilter(new EqualsFilter('productId', $productId));
    
    return $this->productNoteRepository->search($criteria, $context)->getEntities();
}
```

### Combining Multiple Filters

```php
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;

public function searchProductNotes(string $productId, string $searchTerm, Context $context): ProductNoteCollection
{
    $criteria = new Criteria();
    
    // Combine filters with AND logic
    $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
        new EqualsFilter('productId', $productId),
        new ContainsFilter('note', $searchTerm)
    ]));
    
    return $this->productNoteRepository->search($criteria, $context)->getEntities();
}
```

## Step 3: Using Associations

Associations allow you to load related entity data in a single query, improving performance.

### Loading Product Data

```php
public function getProductNotesWithDetails(string $productId, Context $context): ProductNoteCollection
{
    $criteria = new Criteria();
    $criteria->addFilter(new EqualsFilter('productId', $productId));
    
    // Load related entities
    $criteria->addAssociation('product');
    
    return $this->productNoteRepository->search($criteria, $context)->getEntities();
}
```

## Step 4: Sorting and Pagination

### Sorting Results

```php
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;

public function getProductNotesSorted(string $productId, Context $context): ProductNoteCollection
{
    $criteria = new Criteria();
    $criteria->addFilter(new EqualsFilter('productId', $productId));
    
    // Sort by creation date, the newest first
    $criteria->addSorting(new FieldSorting('createdAt', FieldSorting::DESCENDING));
    
    return $this->productNoteRepository->search($criteria, $context)->getEntities();
}
```

### Pagination for Large Datasets

```php
public function getProductNotesPaginated(string $productId, int $limit, int $offset, Context $context): ProductNoteCollection
{
    $criteria = new Criteria();
    $criteria->addFilter(new EqualsFilter('productId', $productId));
    
    // Set pagination
    $criteria->setLimit($limit);
    $criteria->setOffset($offset);
    
    // Sort by creation date
    $criteria->addSorting(new FieldSorting('createdAt', FieldSorting::DESCENDING));
    
    return $this->productNoteRepository->search($criteria, $context)->getEntities();
}
```

So far, all examples focused on simple and easy-to-understand queries. Once these fundamentals are clear, you can combine them to build more advanced queries.

<Callout title="Processing Large Result Sets" type="info">

For very large datasets, Shopware provides the [RepositoryIterator](https://developer.shopware.com/docs/guides/plugins/plugins/framework/data-handling/reading-data.html#using-the-repositoryiterator), which allows you to process results in batches without loading all entities into memory at once.

This concept is typically used in background jobs, imports, or batch-processing scenarios and will be covered in more detail in the Backend Development Advanced learning path.

</Callout>

## Step 5: Advanced Querying

### Using Search Criteria with Multiple Conditions

This example demonstrates how multiple filters can be combined to express more complex query conditions, such as filtering by a date range.

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

namespace AcademyProductNotes\Service;

use DateTime;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Swag\AcademyProductNotes\Content\ProductNote\ProductNoteCollection;

class ProductNoteService
{
  private const string DATE_FORMAT = 'Y-m-d H:i:s';
  
  public function __construct(
      private readonly EntityRepository $productNoteRepository
  ) {}
  
  public function getProductNotesByDateRange(string $productId, DateTime $startDate, DateTime $endDate, Context $context): ProductNoteCollection
  {
      $criteria = new Criteria();
      
      $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
          new EqualsFilter('productId', $productId),
          new RangeFilter('createdAt', [
              RangeFilter::GTE => $startDate->format(self::DATE_FORMAT),
              RangeFilter::LTE => $endDate->format(self::DATE_FORMAT)
          ])
      ]));
      
      $criteria->addSorting(new FieldSorting('createdAt', FieldSorting::ASCENDING));
      
      return $this->productNoteRepository->search($criteria, $context)->getEntities();
  }
}
```

There are a few range filters that can be used:

- `gte` – Greater than or equal to
- `gt` – Greater than
- `lte` – Less than or equal to
- `lt` – Less than

These are defined in Shopware's [RangeFilter class](https://github.com/shopware/shopware/blob/trunk/src/Core/Framework/DataAbstractionLayer/Search/Filter/RangeFilter.php).

### Counting Results

```php
public function countProductNotes(string $productId, Context $context): int
{
    $criteria = new Criteria();
    $criteria->addFilter(new EqualsFilter('productId', $productId));
    
    return $this->productNoteRepository->searchIds($criteria, $context)->getTotal();
}
```

The `searchIds` method is optimized for cases where you only need the total number of results or entity IDs, without loading full entities. This approach is more performant than `search` because it avoids loading unnecessary data.

## Step 6: Safe Data Access and Best Practices

### Null-Safe Entity Access

```php
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;

public function getProductNoteSafely(string $id, Context $context): ?ProductNoteEntity
{   
    $criteria = new Criteria([$id]);
    $result = $this->productNoteRepository->search($criteria, $context);
            
    return $result->first();
}
```

The `search()` method always returns an `EntitySearchResult` object. Calling `$result->first()` returns the first entity in the result set **or** `null` if no matching entity was found.

For this reason, the return type of the method is `?ProductNoteEntity`, which indicates that it can return either a `ProductNoteEntity` or `null`.

When reading data from the DAL, **not finding an entity is not an error**. It is an expected and valid result that must be handled explicitly.

### Performance Optimization Tips

When working with larger datasets or frequently accessed endpoints, the following best practices help keep your queries performant.

1. **Limit Associations**: Only load associations you actually need
2. **Use Pagination**: For large datasets, always implement pagination
3. **Cache Results**: Consider caching frequently accessed data
4. **Optimize Filters**: Use database indexes for frequently filtered fields

## Practical Example: Preparing Data for a Product Notes Dashboard

Let's create a comprehensive method that demonstrates all the concepts we've learned:

```php
public function getProductNotesDashboard(string $productId, Context $context): array
{
    $criteria = new Criteria();
    $criteria->addFilter(new EqualsFilter('productId', $productId));
    
    // Load associations
    $criteria->addAssociation('product');
    
    // Sort by creation date (newest first)
    $criteria->addSorting(new FieldSorting('createdAt', FieldSorting::DESCENDING));
    
    // Pagination for performance
    $criteria->setLimit(50);

    // Request the full number of matching records for pagination metadata
    $criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_EXACT);
    
    $result = $this->productNoteRepository->search($criteria, $context);
    
    $entities = $result->getEntities();
    $total = $result->getTotal();
    
    return [
        'notes' => $entities,
        'total' => $total,
        'product' => $entities->first()?->getProduct(),
        'hasMore' => $total > 50
    ];
}
```

### Explanation

This method shows how DAL is typically used to **compose a query step by step**.

- The `Criteria` filters product notes by a specific product.
- Associations are explicitly added so related data is available.
- Sorting ensures the most recent notes are returned first.
- `setLimit(50)` limits the loaded result list to 50 product notes.
- The exact total count mode tells Shopware to also calculate the full number of matching records.

The repository returns an `EntitySearchResult`, which contains the matching entities (`getEntities()`) and the total number of results (`getTotal()`).

The important difference is between the **loaded entities** and the **total number of matching records**. The `getTotal()` method itself does not decide whether the value is exact. It returns the total value that Shopware calculated for the search result.

For example, imagine the product has 352 matching product notes:

```php
$criteria->setLimit(50);
// no total count mode
```

In this case, Shopware loads at most 50 entities. Because the default total count mode is `Criteria::TOTAL_COUNT_MODE_NONE`, Shopware does not run an extra count query. The result is usually:

```php
$result->getEntities(); // 50 product notes
$result->getTotal();    // 50
```

Now compare that with the example above:

```php
$criteria->setLimit(50);
$criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_EXACT);
```

Here, `setLimit(50)` still limits the loaded entities to 50. But `TOTAL_COUNT_MODE_EXACT` tells Shopware to calculate the full number of matching records as well:

```php
$result->getEntities(); // 50 product notes
$result->getTotal();    // 352
```

This is why `setTotalCountMode(Criteria::TOTAL_COUNT_MODE_EXACT)` is required in this example. Without it, `hasMore => $total > 50` would not be reliable, because `$total` would not represent the full number of matching product notes.

The returned array is structured in a way that is directly usable. This pattern is commonly used when preparing data.

## Summary

Great! In this learning unit, you learned:

- Read data from custom entities using repositories and criteria.
- Apply filters, sorting, pagination, and associations.
- Handle missing entities safely using null-safe access.
- Prepare queried data for further processing or presentation.

With these skills, you now have a solid foundation for reading data from your custom entities using the DAL.
