Relationship mapping links entities together to reflect how data relates in your domain. Instead of manually writing JOIN queries, you define relationships once using annotations, then ObjectQuel handles the database operations automatically.
ObjectQuel supports four relationship types:
ManyToOne - Many entities reference one entity. Example: Many products belong to one category.
class ProductEntity {
/**
* @Orm\ManyToOne(targetEntity="CategoryEntity")
*/
private CategoryEntity $category;
}
OneToMany - One entity has many related entities. Example: One category has many products.
class CategoryEntity {
/**
* @Orm\OneToMany(targetEntity="ProductEntity", mappedBy="categoryId")
*/
public EntityCollection $products;
}
OneToOne - One entity relates to exactly one other entity. Example: One user has one profile.
// Owning side
class UserEntity {
/**
* @Orm\Column(name="profile_id", type="integer", nullable=true)
*/
private ?int $profileId = null;
/**
* @Orm\OneToOne(targetEntity="ProfileEntity", inversedBy="user")
*/
private ?ProfileEntity $profile = null;
public function setProfile(?ProfileEntity $profile): void {
$this->profile = $profile;
$this->profileId = $profile?->getProfileId();
}
}
// Inverse side
class ProfileEntity {
/**
* @Orm\OneToOne(targetEntity="UserEntity", mappedBy="profileId")
*/
private ?UserEntity $user = null;
}
ManyToMany - Many entities relate to many entities. ObjectQuel uses explicit bridge entities for this (see Entity Bridge guide for details).
Every relationship has two sides. Understanding the difference is critical:
/**
* @Orm\Table(name="products")
*/
class ProductEntity {
/**
* @Orm\Column(name="category_id", type="integer")
*/
private int $categoryId;
/**
* @Orm\ManyToOne(targetEntity="CategoryEntity")
*/
private CategoryEntity $category; // OWNING SIDE - has the foreign key
}
/**
* @Orm\Table(name="categories")
*/
class CategoryEntity {
/**
* @Orm\OneToMany(targetEntity="ProductEntity", mappedBy="categoryId")
*/
public EntityCollection $products; // INVERSE SIDE - referenced by foreign key
}
| Side | Description | Impact |
|---|---|---|
| Owning Side | The entity with the foreign key column | Changes here affect the database |
| Inverse Side | The entity referenced by the foreign key | Changes here are ignored by ObjectQuel |
Critical rule: Only changes to the owning side establish or break relationships. The inverse side collection is purely for navigation.
Control when related entities are loaded using the fetch parameter:
class ProductEntity {
/**
* EAGER - Load immediately when product is loaded
* @Orm\ManyToOne(targetEntity="CategoryEntity", fetch="EAGER")
*/
private CategoryEntity $category;
/**
* LAZY - Load only when accessed
* @Orm\ManyToOne(targetEntity="BrandEntity", fetch="LAZY")
*/
private ?BrandEntity $brand = null;
}
When to use EAGER:
@RequiredRelation (mandatory relationships)When to use LAZY:
Use @RequiredRelation for mandatory relationships:
class ProductEntity {
/**
* Required - every product must have a category
* @Orm\ManyToOne(targetEntity="CategoryEntity", fetch="EAGER")
* @Orm\RequiredRelation
*/
private CategoryEntity $category;
/**
* @Orm\Column(name="category_id", type="integer")
*/
private int $categoryId; // NOT NULL in database
/**
* Optional - product may have a brand
* @Orm\ManyToOne(targetEntity="BrandEntity", fetch="LAZY")
*/
private ?BrandEntity $brand = null;
/**
* @Orm\Column(name="brand_id", type="integer", nullable=true)
*/
private ?int $brandId = null; // NULL allowed
}
@RequiredRelation uses INNER JOIN instead of LEFT JOIN, improving query performance.
OneToMany relationships use EntityCollection to hold related entities:
use Quellabs\ObjectQuel\Collections\EntityCollection;
class CustomerEntity {
/**
* @Orm\OneToMany(targetEntity="OrderEntity", mappedBy="customerId")
*/
public EntityCollection $orders;
public function __construct() {
$this->orders = new EntityCollection();
}
}
// Adding entities
$customer->orders->add($order);
// Removing entities
$customer->orders->removeElement($order);
// Checking if collection contains an entity
if ($customer->orders->contains($order)) {
// ...
}
// Counting items
$count = count($customer->orders);
Use the via keyword to traverse relationships in ObjectQuel queries:
// Query using relationship property
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
range of c is App\\Entity\\CategoryEntity via p.category
retrieve (p, c.name) where c.active = true
");
// Query using explicit join condition
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
range of c is App\\Entity\\CategoryEntity via p.categoryId = c.categoryId
retrieve (p, c.name) where c.active = true
");
// Multi-level traversal
$results = $entityManager->executeQuery("
range of c is App\\Entity\\CustomerEntity
range of o is App\\Entity\\OrderEntity via c.orders
range of i is App\\Entity\\OrderItemEntity via o.items
retrieve (c.email, o.orderId, i.quantity)
where o.orderDate >= :date
", ['date' => '2024-01-01']);
// BAD - Triggers one query per product
$products = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p)
");
foreach($products as $row) {
$product = $row['p'];
echo $product->getCategory()->getName(); // Triggers a query!
}
// GOOD - One query loads everything
$products = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
range of c is App\\Entity\\CategoryEntity via p.category
retrieve (p, c)
");
foreach($products as $row) {
$product = $row['p'];
echo $product->getCategory()->getName(); // No query!
}
// WRONG - Only updates inverse side
$category->products->add($product); // This does nothing!
// CORRECT - Update owning side
$product->setCategory($category); // This persists
$category->products->add($product); // This is just for navigation
// WRONG
class CategoryEntity {
public EntityCollection $products; // Uninitialized!
}
// CORRECT
class CategoryEntity {
public EntityCollection $products;
public function __construct() {
$this->products = new EntityCollection();
}
}
@RequiredRelation for mandatory relationships (INNER JOIN vs LEFT JOIN)