| 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:
def seed_with_location(cls, count: int = 5):
locations = LocationSeeder.seed_database(
count=count,
fields={'type': LocationTypeChoices.BIN, ...}
)
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() with cache_seed=True caches the generated objects including IDs
- Field overrides on subsequent runs collide with cached values
in_order gets cached with the original values, not the new ones
- Result: Foreign keys link to old records, causing unique constraint violations or broken relationships
Symptoms
UniqueViolation errors on re-runs
- Foreign keys pointing to wrong/missing records
- Having to set
cache_seed=False to 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):
bin_locations = LocationSeeder.seed(
count=count,
fields={
'type': LocationTypeChoices.BIN,
'name': ('llm', 'Generate a name for a storage bin...'),
},
)
location_models.Location.objects.bulk_create(bin_locations)
bin_location_ids = [loc.id for loc in bin_locations]
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)
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 with pk=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 with pk=None by default
bulk_create() assigns real IDs after insert
- Setting FK fields directly bypasses the cache conflict
- Works with
cache_seed=True on 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):
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
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
bins = BinSeeder.with_rows(count=5)
fields = FieldSeeder.with_lots(count=5)
warehouses = WarehouseSeeder.with_locations(count=5)
Code Review Checklist
When reviewing advanced FK seeding code, verify the following:
- Correct Method: Uses
seed() not seed_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=True is set on the seeder class
- No Duplicate IDs: Ensures no duplicate constraint violations on re-runs
- Child Type Override: Explicitly sets
type in 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
Related Skills
- seeding: Basic seeding patterns and field configurations
- models: Django model conventions