Entity Bridge

Entity Bridges are ObjectQuel's approach to many-to-many relationships. Instead of hiding join tables, ObjectQuel makes them explicit, first-class entities that you can query, extend, and control.

explanation

What are Entity Bridges?

An Entity Bridge is a regular entity that connects two other entities in a many-to-many relationship. While some ORMs hide join tables, ObjectQuel makes them visible and manageable:

/**
 * @Orm\Table(name="product_tags")
 * @Orm\EntityBridge
 */
class ProductTagEntity {
    /**
     * @Orm\Column(name="product_id", type="integer", primary_key=true)
     */
    private int $productId;

    /**
     * @Orm\Column(name="tag_id", type="integer", primary_key=true)
     */
    private int $tagId;

    /**
     * @Orm\ManyToOne(targetEntity="ProductEntity")
     */
    private ProductEntity $product;

    /**
     * @Orm\ManyToOne(targetEntity="TagEntity")
     */
    private TagEntity $tag;
}

The @Orm\EntityBridge annotation marks the entity as a bridge, helping ObjectQuel optimize queries.

Why Entity Bridges?

Making bridge entities explicit provides several advantages:

  • Store metadata - Add fields like timestamps, user IDs, or status flags directly on the relationship
  • Add business logic - Include methods and validation on the relationship itself
  • Query the relationship - Write queries against the bridge entity to find specific connections
  • Full control - Use all ObjectQuel features (lifecycle events, validation, etc.) on relationships
  • No magic - The bridge table is visible in your codebase, making the schema clear

Adding Metadata to Relationships

Bridge entities can store relationship-specific data:

/**
 * @Orm\Table(name="product_tags")
 * @Orm\EntityBridge
 */
class ProductTagEntity {
    /**
     * @Orm\Column(name="product_id", type="integer", primary_key=true)
     */
    private int $productId;

    /**
     * @Orm\Column(name="tag_id", type="integer", primary_key=true)
     */
    private int $tagId;

    /**
     * @Orm\ManyToOne(targetEntity="ProductEntity")
     */
    private ProductEntity $product;

    /**
     * @Orm\ManyToOne(targetEntity="TagEntity")
     */
    private TagEntity $tag;

    /**
     * @Orm\Column(name="assigned_at", type="datetime")
     */
    private \DateTime $assignedAt;

    /**
     * @Orm\Column(name="assigned_by", type="integer")
     */
    private int $assignedBy;

    /**
     * @Orm\Column(name="priority", type="integer")
     */
    private int $priority = 0;

    // Business logic on the relationship
    public function isRecentlyAssigned(): bool {
        return $this->assignedAt > new \DateTime('-7 days');
    }

    public function wasAssignedBy(int $userId): bool {
        return $this->assignedBy === $userId;
    }
}

Working with Bridge Entities

Bridge entities are used like any other entity:

// Creating a relationship
$bridge = new ProductTagEntity();
$bridge->setProduct($product);
$bridge->setTag($tag);
$bridge->setAssignedAt(new \DateTime());
$bridge->setAssignedBy($userId);
$bridge->setPriority(5);

$entityManager->persist($bridge);
$entityManager->flush();

// Removing a relationship
$entityManager->remove($bridge);
$entityManager->flush();

// Finding relationships
$assignments = $entityManager->executeQuery("
    range of pt is App\\Entity\\ProductTagEntity
    range of p is App\\Entity\\ProductEntity via pt.product
    range of t is App\\Entity\\TagEntity via pt.tag
    retrieve (pt, p.name, t.name)
    where pt.assignedBy = :userId
    sort by pt.assignedAt desc
", ['userId' => 123]);

Querying Bridge Entities

You can query bridge entities directly to analyze relationships:

// Find all products tagged with a specific tag
$results = $entityManager->executeQuery("
    range of pt is App\\Entity\\ProductTagEntity
    range of p is App\\Entity\\ProductEntity via pt.product
    retrieve (p)
    where pt.tagId = :tagId
", ['tagId' => 5]);

// Find recent tag assignments
$results = $entityManager->executeQuery("
    range of pt is App\\Entity\\ProductTagEntity
    retrieve (pt)
    where pt.assignedAt > :date
    sort by pt.assignedAt desc
", ['date' => '2024-01-01']);

// Find all tags for a product
$results = $entityManager->executeQuery("
    range of pt is App\\Entity\\ProductTagEntity
    range of t is App\\Entity\\TagEntity via pt.tag
    retrieve (t)
    where pt.productId = :productId
", ['productId' => 123]);

Bridge Entity Best Practices

  • Use composite primary keys (both foreign keys) for the bridge table
  • Initialize collections in both related entities if you want bidirectional navigation
  • Add indexes on foreign key columns for better query performance
  • Consider adding a unique constraint if the relationship should only exist once
  • Use lifecycle events (@PrePersist, @PostDelete) for relationship-specific logic

Frequently Asked Questions

How is an Entity Bridge different from how other ORMs model many-to-many?

Some ORMs hide the join table entirely and only expose the two related entities. ObjectQuel makes the join table a real, first-class entity you can query, extend with extra columns, and attach business logic and lifecycle events to — nothing about the many-to-many relationship is implicit or hidden.

Can I add extra columns to a many-to-many relationship, like a timestamp or status flag?

Yes — that's one of the main reasons to use a bridge entity instead of a hidden join table. Add the columns directly to the bridge entity, alongside the two foreign keys, and they're available like any other entity property.

Do I have to use a composite primary key on the bridge table?

It's the recommended practice, using both foreign key columns together, since that's what naturally prevents the same pair from being linked twice. If the relationship should only exist once per pair, add a unique constraint on top as well.

Does the @Orm\EntityBridge annotation change how the entity behaves?

It marks the entity as a bridge so ObjectQuel can optimize queries involving it. The entity otherwise behaves exactly like any other entity — you persist, remove, and query it the same way.