Working with Relationships
Learn how to define and query related data using foreign keys and the populate feature.
Overviewβ
Relationships allow you to connect data across multiple tables, just like in a traditional relational database. Taruvi supports:
- π One-to-Many: One user has many posts
- π Many-to-One: Many posts belong to one user
- π Many-to-Many: Posts have many tags, tags have many posts
The populate parameter lets you fetch related data in a single request, eliminating the need for multiple round-trips.
Defining Relationshipsβ
Foreign Keys in Schemaβ
Define relationships using the foreignKeys property in your table schema:
{
"name": "posts",
"schema": {
"fields": [
{
"name": "id",
"type": "integer",
"constraints": {"required": true}
},
{
"name": "author_id",
"type": "integer",
"constraints": {"required": true}
},
{
"name": "title",
"type": "string"
}
],
"primaryKey": ["id"],
"foreignKeys": [
{
"fields": ["author_id"],
"reference": {
"resource": "users",
"fields": ["id"]
}
}
]
}
}
Relationship Typesβ
One-to-Many (Has Many)β
A user has many posts:
// users table
{
"fields": [
{"name": "id", "type": "integer"},
{"name": "username", "type": "string"}
],
"primaryKey": ["id"]
}
// posts table
{
"fields": [
{"name": "id", "type": "integer"},
{"name": "author_id", "type": "integer"},
{"name": "title", "type": "string"}
],
"primaryKey": ["id"],
"foreignKeys": [
{
"fields": ["author_id"],
"reference": {
"resource": "users",
"fields": ["id"]
}
}
]
}
Many-to-One (Belongs To)β
A post belongs to one user (inverse of above).
Many-to-Manyβ
Posts have many tags through a join table:
// posts table
{
"fields": [
{"name": "id", "type": "integer"},
{"name": "title", "type": "string"}
],
"primaryKey": ["id"]
}
// tags table
{
"fields": [
{"name": "id", "type": "integer"},
{"name": "name", "type": "string"}
],
"primaryKey": ["id"]
}
// post_tags join table
{
"fields": [
{"name": "post_id", "type": "integer"},
{"name": "tag_id", "type": "integer"}
],
"primaryKey": ["post_id", "tag_id"],
"foreignKeys": [
{
"fields": ["post_id"],
"reference": {"resource": "posts", "fields": ["id"]}
},
{
"fields": ["tag_id"],
"reference": {"resource": "tags", "fields": ["id"]}
}
]
}
Populating Related Dataβ
Use the populate parameter to include related data in your response.
Basic Populationβ
Forward Relationship (Many-to-One)β
Get posts with their authors:
GET /api/apps/blog-app/datatables/posts/data/?populate=author
Response:
{
"data": [
{
"id": 1,
"author_id": 10,
"title": "Getting Started with Taruvi",
"author": {
"id": 10,
"username": "john_doe",
"email": "john@example.com"
}
},
{
"id": 2,
"author_id": 10,
"title": "Advanced Features",
"author": {
"id": 10,
"username": "john_doe",
"email": "john@example.com"
}
}
],
"total": 2
}
The author field is automatically populated based on the author_id foreign key.
Reverse Relationship (One-to-Many)β
Get users with their posts:
GET /api/apps/blog-app/datatables/users/data/?populate=posts
Response:
{
"data": [
{
"id": 10,
"username": "john_doe",
"email": "john@example.com",
"posts": [
{
"id": 1,
"author_id": 10,
"title": "Getting Started with Taruvi"
},
{
"id": 2,
"author_id": 10,
"title": "Advanced Features"
}
]
}
],
"total": 1
}
Multiple Relationshipsβ
Populate multiple relationships at once:
GET /api/apps/blog-app/datatables/posts/data/?populate=author,comments
Response:
{
"data": [
{
"id": 1,
"author_id": 10,
"title": "Getting Started with Taruvi",
"author": {
"id": 10,
"username": "john_doe"
},
"comments": [
{
"id": 101,
"post_id": 1,
"user_id": 20,
"content": "Great post!"
},
{
"id": 102,
"post_id": 1,
"user_id": 30,
"content": "Very helpful, thanks!"
}
]
}
],
"total": 1
}
Nested Populationβ
Populate relationships of relationships (up to 3 levels deep by default):
GET /api/apps/blog-app/datatables/posts/data/?populate=author,comments.user
Response:
{
"data": [
{
"id": 1,
"title": "Getting Started with Taruvi",
"author": {
"id": 10,
"username": "john_doe"
},
"comments": [
{
"id": 101,
"content": "Great post!",
"user": {
"id": 20,
"username": "jane_smith"
}
},
{
"id": 102,
"content": "Very helpful!",
"user": {
"id": 30,
"username": "bob_jones"
}
}
]
}
],
"total": 1
}
Syntax: Use dot notation to populate nested relationships.
Advanced Population Parametersβ
Control how related data is fetched:
GET /api/apps/blog-app/datatables/users/data/?\
populate=posts&\
posts_sort=created_at&\
posts_order=desc&\
posts_limit=5
This:
- Gets users
- Populates their posts
- Sorts posts by created_at (newest first)
- Limits to 5 most recent posts per user
Parameters:
{relationship}_sort: Field to sort by{relationship}_order:ascordesc{relationship}_limit: Maximum number of related records{relationship}_offset: Skip first N related records
Real-World Examplesβ
Blog Post with Full Detailsβ
Get a post with author and comments (including comment authors):
GET /api/apps/blog-app/datatables/posts/data/1/?populate=author,comments.user
Response:
{
"data": {
"id": 1,
"title": "Getting Started with Taruvi",
"content": "In this post, we'll explore...",
"created_at": "2024-01-15T10:30:00Z",
"author": {
"id": 10,
"username": "john_doe",
"bio": "Software developer"
},
"comments": [
{
"id": 101,
"content": "Great post!",
"created_at": "2024-01-15T14:20:00Z",
"user": {
"id": 20,
"username": "jane_smith"
}
},
{
"id": 102,
"content": "Very helpful, thanks!",
"created_at": "2024-01-15T16:45:00Z",
"user": {
"id": 30,
"username": "bob_jones"
}
}
]
}
}
E-commerce Orderβ
Get an order with customer details and order items (including products):
GET /api/apps/shop-app/datatables/orders/data/42/?\
populate=customer,items.product
Response:
{
"data": {
"id": 42,
"order_number": "ORD-2024-00042",
"total": 129.98,
"status": "shipped",
"customer": {
"id": 100,
"name": "Alice Johnson",
"email": "alice@example.com"
},
"items": [
{
"id": 201,
"quantity": 2,
"price": 49.99,
"product": {
"id": 50,
"name": "Widget Pro",
"sku": "WGT-PRO-001"
}
},
{
"id": 202,
"quantity": 1,
"price": 29.99,
"product": {
"id": 51,
"name": "Gadget Lite",
"sku": "GDG-LIT-002"
}
}
]
}
}
User Dashboardβ
Get user with their recent posts and latest comments:
GET /api/apps/blog-app/datatables/users/data/10/?\
populate=posts,comments&\
posts_sort=created_at&\
posts_order=desc&\
posts_limit=5&\
comments_sort=created_at&\
comments_order=desc&\
comments_limit=10
Performance Considerationsβ
Single Query Executionβ
The populate feature uses optimized SQL joins (LATERAL joins for PostgreSQL) to fetch all data in a single database query, which is much faster than making separate requests:
# β N+1 Query Problem (multiple requests)
GET /api/apps/blog-app/datatables/posts/data/
GET /api/apps/blog-app/datatables/users/data/10/
GET /api/apps/blog-app/datatables/users/data/20/
GET /api/apps/blog-app/datatables/users/data/30/
# ... one request per author
# β
Single Request with Populate
GET /api/apps/blog-app/datatables/posts/data/?populate=author
# Returns all posts with authors in one query!
Depth Limitsβ
To prevent performance issues, nesting is limited to 3 levels by default:
# β
Allowed: 2 levels
?populate=author,comments.user
# β
Allowed: 3 levels
?populate=posts.comments.user
# β Too deep: 4+ levels
?populate=posts.comments.user.posts
Contact your administrator if you need deeper nesting.
Limit Related Recordsβ
When populating one-to-many relationships, limit the results:
# β
Good: Limit comments per post
?populate=comments&comments_limit=10
# β Bad: Could return thousands of comments
?populate=comments
Client Integrationβ
JavaScript Exampleβ
async function fetchPostWithDetails(postId) {
const response = await fetch(
`/api/apps/blog-app/datatables/posts/data/${postId}/?populate=author,comments.user`,
{
headers: {
'Authorization': `Bearer ${token}`,
},
}
);
const post = await response.json();
// Access nested data
console.log(`Post by ${post.author.username}`);
console.log(`${post.comments.length} comments`);
post.comments.forEach(comment => {
console.log(`- ${comment.user.username}: ${comment.content}`);
});
return post;
}
React Componentβ
import { useEffect, useState } from 'react';
function PostDetail({ postId }) {
const [post, setPost] = useState(null);
useEffect(() => {
fetch(`/api/apps/blog-app/datatables/posts/data/${postId}/?populate=author,comments.user`)
.then(res => res.json())
.then(data => setPost(data));
}, [postId]);
if (!post) return <div>Loading...</div>;
return (
<article>
<h1>{post.title}</h1>
<p>By {post.author.username}</p>
<div>{post.content}</div>
<h2>Comments ({post.comments.length})</h2>
{post.comments.map(comment => (
<div key={comment.id}>
<strong>{comment.user.username}</strong>: {comment.content}
</div>
))}
</article>
);
}
Python Exampleβ
import requests
def fetch_user_with_posts(user_id, token):
response = requests.get(
f'https://api.yourapp.com/api/apps/blog-app/datatables/users/data/{user_id}/',
params={
'populate': 'posts,comments',
'posts_sort': 'created_at',
'posts_order': 'desc',
'posts_limit': 10
},
headers={'Authorization': f'Bearer {token}'}
)
user = response.json()
print(f"User: {user['username']}")
print(f"Posts: {len(user['posts'])}")
print(f"Comments: {len(user['comments'])}")
return user
Troubleshootingβ
"Relationship 'X' not found"β
Cause: The relationship name doesn't match any foreign key.
Solution: Check your schema's foreignKeys definition. The relationship name should match the field name (without _id suffix) or the referenced resource name.
"Maximum nesting depth exceeded"β
Cause: You're trying to populate more than 3 levels deep.
Solution: Reduce nesting or make separate requests for deeply nested data.
Slow Performanceβ
Cause: Populating too many related records or too deeply nested.
Solution:
- Use
{relationship}_limitto restrict results - Only populate relationships you actually need
- Consider making separate requests for large datasets
Best Practicesβ
1. Only Populate What You Needβ
# β Bad: Populates everything
?populate=author,comments,tags,categories,related_posts
# β
Good: Only what you'll display
?populate=author,comments
2. Limit Collection Sizesβ
# β
Always limit one-to-many relationships
?populate=posts&posts_limit=10
3. Use Specific Fields (Future Feature)β
# Coming soon: Select specific fields from related data
?populate=author&author_fields=id,username
4. Cache Populated Dataβ
Popular posts with comments don't change frequentlyβcache them:
const cache = new Map();
async function fetchWithCache(url, ttl = 60000) {
if (cache.has(url)) {
const { data, timestamp } = cache.get(url);
if (Date.now() - timestamp < ttl) {
return data;
}
}
const response = await fetch(url);
const data = await response.json();
cache.set(url, { data, timestamp: Date.now() });
return data;
}
Common Patternsβ
Blog Homepageβ
GET /api/apps/blog-app/datatables/posts/data/?\
status=published&\
populate=author&\
_sort=published_at&\
_order=desc&\
_start=0&\
_end=10
User Profileβ
GET /api/apps/blog-app/datatables/users/data/10/?\
populate=posts,comments&\
posts_limit=5&\
comments_limit=10
Product Pageβ
GET /api/apps/shop-app/datatables/products/data/42/?\
populate=category,reviews.user,related_products
Activity Feedβ
GET /api/apps/social-app/datatables/activities/data/?\
user_id=10&\
populate=actor,target&\
_sort=created_at&\
_order=desc&\
_start=0&\
_end=20
Next Stepsβ
- Querying Data: Master filtering and sorting
- Creating Data: Insert records with relationships
- Schema Guide: Define foreign keys properly