Quick Answer
Stop data integrity nightmares (orphaned records, impossible deletions). Many2One: Record points to ONE parent (order→customer, line→order). Use ondelete='restrict' (can't delete if children exist), ondelete='cascade' (delete children too), or ondelete='set null' (clear field). One2Many: Inverse (customer→orders), no database column, data stored in Many2One. Many2Many: Bidirectional (product↔tags), uses junction table. Wrong relations = deleted customer orphans 10,000 orders, $30k-$80k cleanup. Right: clean structure, scales to millions of records.
The Relational Problem
Your D2C brand structure:
❏ Customers (1,000+ customers)
❏ Orders (10,000+ orders, each linked to ONE customer)
❏ Order line items (50,000+ lines, each linked to ONE order)
❏ Products (500 products, each with MULTIPLE tags)
❏ Shipping methods (selected by MULTIPLE shipping providers)
Each relationship is different. Get them right, and your data structure is clean and searchable. Get them wrong, and you have orphaned records, impossible deletions, and data integrity nightmares.
We've implemented 150+ Odoo systems. The ones where developers master relational fields? Their systems scale to millions of records with perfect data integrity. The ones who don't? They've deleted a customer and somehow orphaned 10,000 orders. That's $30,000-$80,000 in data cleanup and emergency consulting.
The Three Relational Types
| Type | Description | Example |
|---|---|---|
| Many2One | Record points to ONE parent | Order → Customer |
| One2Many | Show all children (inverse) | Customer → Orders |
| Many2Many | Bidirectional multiple links | Product ↔ Tags |
Type 1: Many2One (Link to One Parent Record)
What it is: A record points to ONE record in another model. Multiple records can point to the same parent.
Real D2C example: Order → Customer
Each order belongs to exactly ONE customer. Many orders can belong to the same customer.
class SaleOrder(models.Model):
_inherit = 'sale.order'
# MANY2ONE: This order links to ONE customer
partner_id = fields.Many2one(
'res.partner', # Link to this model
string='Customer',
required=True, # Can't save without customer
ondelete='restrict', # Prevent deleting customer if orders exist
index=True, # Speed up searches by customer
help='Select the customer for this order'
)
# OTHER MANY2ONE EXAMPLES
warehouse_id = fields.Many2one(
'stock.warehouse',
string='Warehouse',
ondelete='set null', # If warehouse deleted, just clear field
help='Warehouse that will ship this order'
)
salesman_id = fields.Many2one(
'res.users',
string='Sales Person',
ondelete='set null', # If user deleted, clear field (don't break order)
)
Accessing Related Records
order = self.env['sale.order'].browse(1000)
# Access the customer
customer_name = order.partner_id.name # "Acme Inc"
customer_email = order.partner_id.email # "contact@acme.com"
# Modify the customer
order.partner_id.name = 'Acme Corp'
# Change the customer
new_customer = self.env['res.partner'].search([('name', '=', 'New Customer')])
order.partner_id = new_customer
order.save()
ondelete Options (Critical!)
| Option | Behavior |
|---|---|
| restrict | Cannot delete parent if children exist (ERROR) |
| cascade | Delete children automatically when parent deleted |
| set null | Clear field when parent deleted (don't break child) |
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
order_id = fields.Many2one(
'sale.order',
string='Order',
required=True,
ondelete='cascade' # If order deleted, delete this line too
)
product_id = fields.Many2one(
'product.product',
string='Product',
required=True,
ondelete='restrict' # Can't delete product if lines reference it
)
Type 2: One2Many (Show All Child Records)
What it is: Inverse of Many2One. A record can show all related records from another model.
Real D2C example: Customer → Orders
A customer has many orders. Show all orders for this customer.
class ResPartner(models.Model):
_inherit = 'res.partner'
# ONE2MANY: Show all orders for this customer
order_ids = fields.One2many(
'sale.order', # Related model
'partner_id', # The Many2one field in sale.order that points back here
string='Orders',
help='All orders from this customer'
)
# OTHER ONE2MANY EXAMPLES
invoice_ids = fields.One2many(
'account.move',
'partner_id',
string='Invoices'
)
activity_ids = fields.One2many(
'mail.activity',
'res_id',
string='Activities'
)
Key points: No database column for One2Many fields. Data is stored in the Many2One field on the related model. inverse_name must match the Many2One field name in the related model.
Accessing Child Records
customer = self.env['res.partner'].browse(5)
# Access all orders for this customer
orders = customer.order_ids # Returns a recordset of all orders
print(f"Customer has {len(orders)} orders")
# Loop through orders
for order in customer.order_ids:
print(f"Order order.name: order.amount_total")
# Filter orders
paid_orders = customer.order_ids.filtered(lambda o: o.state == 'done')
print(f"Paid orders: len(paid_orders)")
# Calculate totals
total_value = sum(o.amount_total for o in customer.order_ids)
print(f"Total customer value: total_value")
Manipulating Child Records
# You can manipulate child records through parent
customer = self.env['res.partner'].browse(5)
# Add a new order through the parent
new_order_data = {
'name': 'Order #New',
'order_date': fields.Date.today(),
'amount_total': 1500,
}
customer.order_ids = [(0, 0, new_order_data)] # Create
# OR
customer.order_ids = [(4, order_id)] # Link existing
# Remove an order
customer.order_ids = [(3, order_id)] # Unlink
Type 3: Many2Many (Bidirectional Multiple Links)
What it is: Multiple records on one side can link to multiple records on the other side.
Real D2C example: Product ↔ Tags
A product can have multiple tags (organic, bestseller, eco-friendly). A tag can be on multiple products.
class ProductProduct(models.Model):
_inherit = 'product.product'
# MANY2MANY: Link to multiple tags
tag_ids = fields.Many2many(
'product.tag', # Related model
'product_tag_rel', # Junction table name
'product_id', # Column in junction table for this model
'tag_id', # Column in junction table for related model
string='Tags',
help='Tags for this product'
)
# ANOTHER MANY2MANY EXAMPLE
collection_ids = fields.Many2many(
'product.collection',
'product_collection_rel',
'product_id',
'collection_id',
string='Collections',
help='Which collections is this product in?'
)
Database Structure
product_product table:
- id (1)
- name ('Organic Cotton T-Shirt')
product_tag table:
- id (10)
- name ('Organic')
product_tag_rel (junction table):
- product_id (1), tag_id (10) ← Links product to tag
- product_id (1), tag_id (11) ← Product has multiple tags
- product_id (2), tag_id (10) ← Tag can be on multiple products
Accessing Many2Many Records
product = self.env['product.product'].browse(1)
# Access all tags
tags = product.tag_ids # Recordset of all tags
print(f"Product has {len(tags)} tags")
for tag in product.tag_ids:
print(f"- {tag.name}")
# Add a tag
new_tag = self.env['product.tag'].search([('name', '=', 'Bestseller')])
product.tag_ids = [(4, new_tag.id)] # Add existing tag
# Create and add new tag
product.tag_ids = [(0, 0, {'name': 'New Tag'})] # Create and link
# Remove a tag
product.tag_ids = [(3, tag_id)] # Unlink tag
# Replace all tags
product.tag_ids = [(6, 0, [tag1.id, tag2.id])] # Replace with these tags only
Real D2C Example: Complete Relationship Structure
class Customer(models.Model):
_inherit = 'res.partner'
# MANY2ONE: Customer belongs to one account manager
account_manager_id = fields.Many2one(
'res.users',
string='Account Manager',
ondelete='set null',
help='Who manages this account?'
)
# ONE2MANY: Customer has many orders
order_ids = fields.One2many(
'sale.order',
'partner_id',
string='Orders',
)
# MANY2MANY: Customer has many preferred payment methods
payment_method_ids = fields.Many2many(
'payment.method',
'customer_payment_rel',
'customer_id',
'payment_id',
string='Preferred Payment Methods'
)
class Order(models.Model):
_inherit = 'sale.order'
# MANY2ONE: Order belongs to one customer
partner_id = fields.Many2one(
'res.partner',
string='Customer',
required=True,
ondelete='restrict'
)
# MANY2ONE: Order is in one warehouse
warehouse_id = fields.Many2one(
'stock.warehouse',
string='Warehouse',
ondelete='set null'
)
# ONE2MANY: Order has many line items
order_line_ids = fields.One2many(
'sale.order.line',
'order_id',
string='Order Lines'
)
# MANY2MANY: Order uses many shipping methods
available_shipping_ids = fields.Many2many(
'shipping.method',
'order_shipping_rel',
'order_id',
'shipping_id',
string='Available Shipping Methods'
)
class OrderLine(models.Model):
_inherit = 'sale.order.line'
# MANY2ONE: Line belongs to one order
order_id = fields.Many2one(
'sale.order',
string='Order',
required=True,
ondelete='cascade' # Delete line if order deleted
)
# MANY2ONE: Line is for one product
product_id = fields.Many2one(
'product.product',
string='Product',
required=True,
ondelete='restrict' # Can't delete product if lines reference it
)
ondelete Behavior Comparison
| Scenario | ondelete='restrict' | ondelete='cascade' | ondelete='set null' |
|---|---|---|---|
| Delete customer with orders | ERROR - Cannot delete | All orders deleted too | partner_id cleared in orders |
| Delete order with lines | ERROR - Cannot delete | All lines deleted too | order_id cleared in lines |
| Delete product in lines | ERROR - Cannot delete | All lines deleted too | product_id cleared in lines |
Action Items: Master Relational Fields
Design Your Relationships
❏ Map all relationships: customer → order → line
❏ Decide: Many2One, One2Many, or Many2Many?
❏ Set ondelete appropriately (restrict, cascade, set null)
❏ Add index=True to frequently-searched Many2One fields
Implement Correctly
❏ Many2One defines the relationship
❏ One2Many is the inverse (automatically created)
❏ Many2Many needs both sides to work
❏ Test delete behavior before production
Optimize Performance
❏ Index Many2One fields used in searches
❏ Use mapped() to extract fields from related records
❏ Avoid N+1 queries on relational fields
❏ Cache computed totals (stored computed fields)
Frequently Asked Questions
What's the difference between Many2One and One2Many?
Many2One: Child record points to ONE parent (order→customer). Creates database column. One2Many: Inverse—parent sees all children (customer→orders). No database column, data stored in Many2One field on related model. Many2One defines the relationship, One2Many is just the reverse view.
When should I use ondelete='cascade' vs ondelete='restrict'?
cascade: Use when child record has no meaning without parent (order line without order). Deleting parent auto-deletes children. restrict: Use when deleting parent would break important data (customer with orders, product in order lines). Prevents deletion unless children removed first. set null: Use when relationship is optional (warehouse deleted, just clear warehouse_id in orders).
How does Many2Many work in the database?
Many2Many creates a junction table (e.g., product_tag_rel) with two columns: product_id and tag_id. Each row links one product to one tag. Product can have multiple tags (multiple rows with same product_id). Tag can be on multiple products (multiple rows with same tag_id). Both sides automatically updated—add tag to product, product automatically appears in tag.product_ids.
What happens if I delete a customer with 10,000 orders?
Depends on ondelete: restrict: ERROR—cannot delete customer (must delete/reassign orders first). cascade: All 10,000 orders deleted automatically (dangerous!). set null: Customer deleted, partner_id cleared in all 10,000 orders (orphaned orders with no customer). For customers, always use restrict to prevent data loss.
Free Data Structure Design Workshop
Stop building broken data models. We'll map your business relationships, design optimal relational fields, set up proper delete behavior, create performant searches, and document your data structure. Most D2C brands discover their relationships are wrong after they have 100,000 records. Getting it right upfront saves $25,000-$60,000 in data cleanup.
