Soft Delete

Soft delete lets you mark entities as deleted without removing them from the database. Deleted records are automatically hidden from queries and can be restored at any time by clearing the delete marker.

explanation

What is Soft Delete?

A normal delete removes a row from the database permanently. A soft delete instead sets a column on the row — a timestamp or a boolean — to indicate that the record is no longer active. ObjectQuel filters those rows out of every query automatically, so the rest of your application sees no difference:

// Hard delete — row is gone permanently
$entityManager->remove($order);
$entityManager->flush();

// Soft delete — row stays in the database, but disappears from queries
$order->setDeletedAt(new \DateTime());
$entityManager->flush();

When soft delete is useful:

  • You need an audit trail of deleted records
  • Deletions should be recoverable by an administrator
  • Regulatory requirements prohibit permanent data removal
  • You want a recycle-bin or undo-delete feature

When a hard delete is better:

  • The data is genuinely disposable (e.g. session records, temporary rows)
  • Privacy regulations require true erasure (e.g. GDPR right to erasure)
  • Table size is a concern and deleted rows should not accumulate

Annotating an Entity

Add a nullable column to your entity and mark it with @Orm\SoftDelete alongside the usual @Orm\Column. The annotation must sit on a column property — it has no effect on its own without a backing column.

ObjectQuel supports two column types for the soft-delete marker:

  • datetimeNULL means active; any timestamp means deleted. This is the recommended type because it records when the record was deleted.
  • booleanfalse means active; true means deleted.
use Quellabs\ObjectQuel\Annotations\Orm\Column;
use Quellabs\ObjectQuel\Annotations\Orm\SoftDelete;

class OrderEntity {

    // ... other properties ...

    /**
     * @Orm\Column(name="deleted_at", type="datetime", nullable=true)
     * @Orm\SoftDelete
     */
    protected ?\DateTime $deletedAt = null;

    public function getDeletedAt(): ?\DateTime {
        return $this->deletedAt;
    }

    public function setDeletedAt(?\DateTime $deletedAt): self {
        $this->deletedAt = $deletedAt;
        return $this;
    }
}

Marking and Restoring

Soft delete is a regular property update. Set the column value and call flush() — no special methods needed:

// Mark as deleted
$order->setDeletedAt(new \DateTime());
$entityManager->persist($order);
$entityManager->flush();

// Restore — set back to null
$order->setDeletedAt(null);
$entityManager->persist($order);
$entityManager->flush();

For boolean soft-delete columns the pattern is identical:

// Mark as deleted
$order->setDeleted(true);
$entityManager->persist($order);
$entityManager->flush();

// Restore
$order->setDeleted(false);
$entityManager->flush();

Automatic Filtering

Once an entity has a @SoftDelete annotation, ObjectQuel adds the filter condition to every query for that entity automatically. No changes to your queries are required:

// Returns only active orders — soft-deleted rows are excluded automatically
$orders = $entityManager->executeQuery("
    range of o is App\Entity\OrderEntity
    retrieve (o)
");

// Conditions and joins work the same way
$orders = $entityManager->executeQuery("
    range of o is App\Entity\OrderEntity
    range of c is App\Entity\CustomerEntity via o.customer
    retrieve (o) where c.id = :customerId
", ['customerId' => 42]);
Note on joined entities: The soft-delete filter applies independently to every range in a query. If both OrderEntity and CustomerEntity have @SoftDelete, rows are excluded when either entity is soft-deleted.

Bypassing the Filter

There are two ways to see soft-deleted records when you need to.

The @ignoreSoftDelete directive disables the filter for a single query:

// Returns all orders, including soft-deleted ones
$allOrders = $entityManager->executeQuery("
    @ignoreSoftDelete true
    range of o is App\Entity\OrderEntity
    retrieve (o)
");

// Useful for admin views or restore workflows
$deleted = $entityManager->executeQuery("
    @ignoreSoftDelete true
    range of o is App\Entity\OrderEntity
    retrieve (o) where o.deletedAt is not null
");

find() always bypasses the filter. When you look up an entity by primary key, you always get it back regardless of its soft-delete state. This prevents confusing situations where an entity you just modified suddenly becomes unfindable:

// Returns the order even if it has been soft-deleted
$order = $entityManager->find(OrderEntity::class, 99);

if ($order !== null && $order->getDeletedAt() !== null) {
    // order exists but is soft-deleted
}

Hard Delete

Soft delete and hard delete coexist without conflict. remove() always issues a real DELETE statement regardless of whether the entity has a @SoftDelete annotation:

// Permanently removes the row from the database
$entityManager->remove($order);
$entityManager->flush();

This means you can soft-delete an entity for day-to-day use and still permanently purge it when needed — for example during a data retention cleanup job — without any special handling.

Summary

  • Annotation — place @Orm\SoftDelete alongside @Orm\Column on a nullable datetime or boolean property
  • Mark deleted — set the property value and call flush()
  • Restore — set the property back to null / false and call flush()
  • Filter — applied automatically to all queries; no query changes needed
  • Bypass per query — use the @ignoreSoftDelete true compiler directive
  • Bypass for PK lookupfind() always returns the entity regardless of state
  • Hard deleteremove() always issues a real DELETE