name: advanced-fk-seeding description: Advanced seeding patterns for linking specific foreign keys
Advanced Foreign Key Seeding
Overview
This skill covers the pattern for seeding models that require specific, deterministic foreign key relationships — where you need to control exactly which records link to which, rather than relying on random or cached FK assignment.
This pattern resolves cache conflicts that occur when using in_order with cache_seed=True, enabling clean re-seeding without turning off the cache.
The Problem
When seeding related models together (e.g., bins + locations, fields + lots), the old approach used in_order via field overrides:
# OLD PATTERN - causes cache conflicts
def seed_with_location(cls, count: int = 5):
# Step 1: Seed locations
locations = LocationSeeder.seed_database(
count=count,
fields={'type': LocationTypeChoices.BIN, ...}
)
# Step 2: Seed bins with in_order FK (problem here)
location_ids = [loc.id for loc in locations]
return cls.seed_database(
count=count,
fields={
'location_id': ('custom', 'in_order', {'values': location_ids})
}
)
Why It Fails
seed_database()withcache_seed=Truecaches the generated objects including IDs- Field overrides on subsequent runs collide with cached values
in_ordergets cached with the original values, not the new ones- Result: Foreign keys link to old records, causing unique constraint violations or broken relationships
Symptoms
UniqueViolationerrors on re-runs- Foreign keys pointing to wrong/missing records
- Having to set
cache_seed=Falseto work around the issue
The Solution
Use seed() to generate objects without database insertion, then manually manage IDs through bulk_create():
from __future__ import annotations
from app.location import models as location_models
from app.location.bin import models
from app.location.choices import LocationTypeChoices
from app.location.seeding.seeder import LocationSeeder
class BinSeeder(DjangoModelSeeder):
model_class = models.Bin
cache_seed = True
@classmethod
def with_rows(cls, count: int = 5, rows_per_bin: int = 4):
# 1. Create bin location objects with field overrides
bin_locations = LocationSeeder.seed(
count=count,
fields={
'type': LocationTypeChoices.BIN,
'name': ('llm', 'Generate a name for a storage bin...'),
},
)
# 2. Bulk create locations and capture new IDs
location_models.Location.objects.bulk_create(bin_locations)
bin_location_ids = [loc.id for loc in bin_locations]
# 3. Create bin objects, link to locations, bulk create
bins = cls.seed(count=count)
for bin, loc_id in zip(bins, bin_location_ids):
bin.location_id = loc_id
models.Bin.objects.bulk_create(bins)
# 4. Create row child locations linked to bins
total_rows = count * rows_per_bin
row_names = [f'Row {i}' for i in range(1, total_rows + 1)]
bin_ids_expanded = [bin_id for bin_id in bin_location_ids for _ in range(rows_per_bin)]
rows = LocationSeeder.seed(
count=total_rows,
fields={
'type': LocationTypeChoices.ROW,
},
)
for row, parent_id, name in zip(rows, bin_ids_expanded, row_names):
row.parent_id = parent_id
row.name = name
location_models.Location.objects.bulk_create(rows)
return bins
Core Pattern
Step-by-Step
- Call
Seeder.seed()with field overrides — generates objects withpk=None - Call
Model.objects.bulk_create()— inserts records and assigns real DB IDs - Capture IDs from bulk create result —
[obj.id for obj in objects] - Call child seeder's
seed()— generates child objects - Zip parent IDs with children —
for child, parent_id in zip(children, parent_ids) - Set FK fields directly —
child.parent_id = parent_id - Bulk create children
Key Points
seed()returns objects withpk=Noneby defaultbulk_create()assigns real IDs after insert- Setting FK fields directly bypasses the cache conflict
- Works with
cache_seed=Trueon all seeders
Examples
Simple Link (No Children)
For models like Warehouse that link directly to locations:
@classmethod
def with_locations(cls, count: int = 5):
warehouse_locations = LocationSeeder.seed(
count=count,
fields={
'type': LocationTypeChoices.WAREHOUSE,
'name': ('llm', 'Generate a name for a warehouse...'),
},
)
location_models.Location.objects.bulk_create(warehouse_locations)
warehouse_location_ids = [loc.id for loc in warehouse_locations]
warehouses = cls.seed(count=count)
for warehouse, loc_id in zip(warehouses, warehouse_location_ids):
warehouse.location_id = loc_id
models.Warehouse.objects.bulk_create(warehouses)
return warehouses
With Children (Parent + Multiple Children Per Parent)
For models like Bin that have multiple row children per bin:
@classmethod
def with_rows(cls, count: int = 5, rows_per_bin: int = 4):
# ... create and save parent locations ...
# Create rows with expanded parent IDs
total_rows = count * rows_per_bin
row_names = [f'Row {i}' for i in range(1, total_rows + 1)]
bin_ids_expanded = [bin_id for bin_id in bin_location_ids for _ in range(rows_per_bin)]
rows = LocationSeeder.seed(count=total_rows)
for row, parent_id, name in zip(rows, bin_ids_expanded, row_names):
row.parent_id = parent_id
row.name = name
location_models.Location.objects.bulk_create(rows)
Setting Type in Loop (When Field Override Doesn't Apply)
Some fields like type may not apply via field override due to caching. Set them explicitly:
lots = LocationSeeder.seed(
count=total_lots,
fields={
'type': LocationTypeChoices.LOT,
},
)
for lot, parent_id, name in zip(lots, field_ids_expanded, lot_names):
lot.type = LocationTypeChoices.LOT # Explicitly set, not from field override
lot.parent_id = parent_id
lot.name = name
location_models.Location.objects.bulk_create(lots)
Usage in seed.py
from __future__ import annotations
from app.location.bin.seeding.seeder import BinSeeder
from app.location.field.seeding.seeder import FieldSeeder
from app.location.warehouse.seeding.seeder import WarehouseSeeder
# Seed bins with child rows
bins = BinSeeder.with_rows(count=5)
# Seed fields with child lots
fields = FieldSeeder.with_lots(count=5)
# Seed warehouses
warehouses = WarehouseSeeder.with_locations(count=5)
Code Review Checklist
When reviewing advanced FK seeding code, verify the following:
- Correct Method: Uses
seed()notseed_database()for initial object generation - Bulk Create Order: Parent objects are bulk created before capturing IDs
- ID Capture: IDs are captured from bulk_create result, not from original objects
- Zip Pattern: Uses
zip()to pair children with parent IDs - Expanded IDs: For multiple children per parent, IDs are expanded with list comprehension
- Direct FK Assignment: FK fields are set directly on objects before bulk_create
- Return Value: Method returns parent objects when appropriate
- Cache Config:
cache_seed=Trueis set on the seeder class - No Duplicate IDs: Ensures no duplicate constraint violations on re-runs
- Child Type Override: Explicitly sets
typein loop when field override doesn't apply - Consistent Naming: Method names follow pattern (
with_locations,with_rows,with_lots) - Comments: Step comments explain each phase of the seeding process