Skip to main content

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"]}
}
]
}

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:

  1. Gets users
  2. Populates their posts
  3. Sorts posts by created_at (newest first)
  4. Limits to 5 most recent posts per user

Parameters:

  • {relationship}_sort: Field to sort by
  • {relationship}_order: asc or desc
  • {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.

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}_limit to 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​