Relations and References
In dbzero, relationships between your data objects—or memos—can be either direct or indirect. While direct references are simple and straightforward to use, it's the indirect relationships built with tags that unlock dbzero's potential for creating modular, decoupled applications.
This chapter focuses on how to use these two patterns to build clean and maintainable systems.
Direct References: Tight Coupling
The most obvious way to relate two memos is by making one an attribute of the other. This is identical to how you work with standard Python objects and is perfect for relationships where one object logically "owns" another.
Consider an e-commerce system. An Order naturally contains a list of LineItem objects.
@db0.memo
class LineItem:
def __init__(self, product_id, quantity, price):
self.product_id = product_id
self.quantity = quantity
self.price = price
@db0.memo
class Order:
def __init__(self, customer_id, items):
self.customer_id = customer_id
# A direct reference to a list of LineItem memos
self.items = items
# --- Usage ---
# The code that creates an Order must also create LineItems.
# They are tightly coupled.
line_items = [LineItem("prod-123", 2, 19.99), LineItem("prod-456", 1, 49.99)]
order = Order("cust-abc", line_items)
# Access is straightforward
total_items = sum(item.quantity for item in order.items)This pattern is simple and effective. Its key characteristic is tight coupling: the Order class and the code that uses it have a direct dependency on the LineItem class. This is appropriate when the relationship is a core, inseparable part of your domain model.
Indirect References: Decoupling at a Distance
What happens when different parts of your application need to collaborate without having direct knowledge of each other? Forcing them to share information via function arguments creates brittle, complex code.
Tags solve this by allowing persistent objects to be discovered by any part of the system that knows how to look for them.
Example: A Decoupled Promotions Engine
Imagine an e-commerce platform with three independent business applications, each managed by a different team:
- Product Catalog: Manages all product information.
- Promotions Engine: Creates and manages discount codes.
- Checkout Service: Processes customer purchases.
The Promotions Engine needs to create discounts that apply to certain products, but it shouldn't have to know about every single product. The Checkout Service needs to find applicable discounts for a customer's cart.
Tags provide the link.
1. The Product Catalog Module
This module defines products and tags them with relevant attributes.
# In product_catalog/models.py
@db0.memo
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
# In product_catalog/logic.py
def add_new_product(name, price, attributes):
product = Product(name, price)
# Tag the product with its characteristics
db0.tags(product).add(*attributes)
print(f"Added product: {name}")
# The product team adds fishing equipment
add_new_product("Pro Fishing Rod", 120.00, ["category:fishing", "brand:acme", "sporting_goods"])2. The Promotions Engine Module
This module defines generic PromoCode objects. Crucially, these promos don't reference products directly; they only specify the tags a product must have to be eligible.
# In promotions/models.py
@db0.memo
class PromoCode:
def __init__(self, code, discount_percent, required_tags):
self.code = code
self.discount_percent = discount_percent
# A list of tags a product must have for this promo to apply
self.required_tags = set(required_tags)
# The marketing team creates a promo for fishing gear,
# without knowing anything about specific product models.
promo = PromoCode("SUMMERFISH15", 15, required_tags=["category:fishing"])
db0.tags(promo).add("active_promo")3. The Checkout Service
Here, in a distant part of the application, we can bring it all together. When a customer adds a product to their cart, the checkout service can discover applicable promotions without having a direct reference to the promotions engine.
# In checkout/logic.py
def get_applicable_promos_for(product):
# Find all active promotions in the entire system
all_active_promos = db0.find(PromoCode, "active_promo")
# Check which ones apply to the given product
for promo in all_active_promos:
if db0.find(product, tuple(promo.required_tags)):
yield promo
# --- In the main application flow ---
# A customer wants to buy the fishing rod
rod = next(iter(db0.filter(lambda x: x.name == "Pro Fishing Rod", db0.find(Product))))
for p in get_applicable_promos_for(rod):
print(f"Found applicable promo: {p.code} ({p.discount_percent}% off!)")
# Output: Found applicable promo: SUMMERFISH15 (15% off!)True Decoupling: The three modules operate independently.
- The Promotions team can add or change promotions targeting any set of tags (
"black_friday","clearance") without deploying changes to the Product or Checkout code. - The Product team can add new items. If a new item is tagged with
"category:fishing", it automatically becomes eligible for theSUMMERFISH15discount. - The Checkout service remains simple; its only job is to match the tags between the product and the available promotions.