Working with Relations - Kotlin SDK Documentation

Overview

Relations allow you to link records between collections. BosBase supports both single and multiple relations, and provides powerful features for expanding related records and working with back-relations.

Key Features:

  • Single and multiple relations
  • Expand related records without additional requests
  • Nested relation expansion (up to 6 levels)
  • Back-relations for reverse lookups
  • Field modifiers for append/prepend/remove operations

Relation Field Types:

  • Single Relation: Links to one record (MaxSelect <= 1)
  • Multiple Relation: Links to multiple records (MaxSelect > 1)

Backend Behavior:

  • Relations are stored as record IDs or arrays of IDs
  • Expand only includes relations the client can view (satisfies View API Rule)
  • Back-relations use format: collectionName_via_fieldName
  • Back-relation expand limited to 1000 records per field

📖 Reference: For detailed relations concepts, see the JavaScript SDK Relations documentation.

Setting Up Relations

Creating a Relation Field

import com.bosbase.sdk.BosBase

val pb = BosBase("http://localhost:8090")
pb.admins.authWithPassword("admin@example.com", "password")

val collection = pb.collections.getOne("posts")
val fields = (collection["fields"]?.jsonArray?.toMutableList() ?: mutableListOf()).toMutableList()

// Single relation field
fields.add(
    mapOf(
        "name" to "user",
        "type" to "relation",
        "options" to mapOf(
            "collectionId" to "users",  // ID of related collection
            "maxSelect" to 1           // Single relation
        ),
        "required" to true
    )
)

// Multiple relation field
fields.add(
    mapOf(
        "name" to "tags",
        "type" to "relation",
        "options" to mapOf(
            "collectionId" to "tags",
            "maxSelect" to 10,          // Multiple relation (max 10)
            "minSelect" to 1            // Minimum 1 required
        ),
        "cascadeDelete" to false       // Don't delete post when tags deleted
    )
)

pb.collections.update("posts", body = mapOf("fields" to fields))

Creating Records with Relations

Single Relation

// Create a post with a single user relation
val post = pb.collection("posts").create(
    body = mapOf(
        "title" to "My Post",
        "user" to "USER_ID"  // Single relation ID
    )
)

Multiple Relations

// Create a post with multiple tags
val post = pb.collection("posts").create(
    body = mapOf(
        "title" to "My Post",
        "tags" to listOf("TAG_ID1", "TAG_ID2", "TAG_ID3")  // Array of IDs
    )
)

Mixed Relations

// Create a comment with both single and multiple relations
val comment = pb.collection("comments").create(
    body = mapOf(
        "message" to "Great post!",
        "post" to "POST_ID",        // Single relation
        "user" to "USER_ID",        // Single relation
        "tags" to listOf("TAG1", "TAG2")  // Multiple relation
    )
)

Updating Relations

Replace All Relations

// Replace all tags
pb.collection("posts").update(
    id = "POST_ID",
    body = mapOf("tags" to listOf("NEW_TAG1", "NEW_TAG2"))
)

Append Relations (Using + Modifier)

// Append tags to existing ones
pb.collection("posts").update(
    id = "POST_ID",
    body = mapOf("tags+" to "NEW_TAG_ID")  // Append single tag
)

// Append multiple tags
pb.collection("posts").update(
    id = "POST_ID",
    body = mapOf("tags+" to listOf("TAG_ID1", "TAG_ID2"))  // Append multiple tags
)

Prepend Relations (Using + Prefix)

// Prepend tags (tags will appear first)
pb.collection("posts").update(
    id = "POST_ID",
    body = mapOf("+tags" to "PRIORITY_TAG")  // Prepend single tag
)

// Prepend multiple tags
pb.collection("posts").update(
    id = "POST_ID",
    body = mapOf("+tags" to listOf("TAG1", "TAG2"))  // Prepend multiple tags
)

Remove Relations (Using - Modifier)

// Remove single tag
pb.collection("posts").update(
    id = "POST_ID",
    body = mapOf("tags-" to "TAG_ID_TO_REMOVE")
)

// Remove multiple tags
pb.collection("posts").update(
    id = "POST_ID",
    body = mapOf("tags-" to listOf("TAG1", "TAG2"))
)

Expanding Relations

Single Relation Expansion

// Get post with author expanded
val post = pb.collection("posts").getOne(
    id = "POST_ID",
    expand = "author"
)

// Access expanded author
val author = post["expand"]?.jsonObject?.get("author")?.jsonObject
val authorName = author?.get("name")?.jsonPrimitive?.contentOrNull
val authorEmail = author?.get("email")?.jsonPrimitive?.contentOrNull

Multiple Relation Expansion

// Expand multiple relations
val post = pb.collection("posts").getOne(
    id = "POST_ID",
    expand = "author,categories"
)

// Access expanded categories (array)
val categories = post["expand"]?.jsonObject?.get("categories")?.jsonArray
categories?.forEach { category ->
    val name = category.jsonObject["name"]?.jsonPrimitive?.contentOrNull
    println("Category: $name")
}

Nested Relation Expansion

// Expand nested relations (up to 6 levels)
val post = pb.collection("posts").getOne(
    id = "POST_ID",
    expand = "author,categories.tags"
)

// Access nested expanded data
val category = post["expand"]?.jsonObject?.get("categories")?.jsonArray?.firstOrNull()
val tags = category?.jsonObject?.get("expand")?.jsonObject?.get("tags")?.jsonArray

Expansion in List Queries

// List posts with expanded relations
val result = pb.collection("posts").getList(
    page = 1,
    perPage = 50,
    expand = "author,categories"
)

result.items.forEach { post ->
    val author = post["expand"]?.jsonObject?.get("author")?.jsonObject
    println("Author: ${author?.get("name")?.jsonPrimitive?.contentOrNull}")
}

Back-Relations

Back-relations allow you to access reverse relationships. Use the format: collectionName_via_fieldName

// Get user with all their posts (back-relation)
val user = pb.collection("users").getOne(
    id = "USER_ID",
    expand = "posts_via_author"  // Back-relation: posts where author = user
)

// Access back-relation
val posts = user["expand"]?.jsonObject?.get("posts_via_author")?.jsonArray
posts?.forEach { post ->
    val title = post.jsonObject["title"]?.jsonPrimitive?.contentOrNull
    println("Post: $title")
}

Complete Examples

Blog Post with Author and Tags

fun createBlogPost(pb: BosBase, title: String, content: String, authorId: String, tagIds: List<String>) {
    // Create post with relations
    val post = pb.collection("posts").create(
        body = mapOf(
            "title" to title,
            "content" to content,
            "author" to authorId,      // Single relation
            "tags" to tagIds           // Multiple relation
        )
    )
    
    println("Created post: ${post["id"]?.jsonPrimitive?.contentOrNull}")
    
    // Get post with relations expanded
    val fullPost = pb.collection("posts").getOne(
        id = post["id"]?.jsonPrimitive?.contentOrNull ?: "",
        expand = "author,tags"
    )
    
    // Access expanded data
    val author = fullPost["expand"]?.jsonObject?.get("author")?.jsonObject
    println("Author: ${author?.get("name")?.jsonPrimitive?.contentOrNull}")
    
    val tags = fullPost["expand"]?.jsonObject?.get("tags")?.jsonArray
    tags?.forEach { tag ->
        println("Tag: ${tag.jsonObject["name"]?.jsonPrimitive?.contentOrNull}")
    }
}

Managing Tags on Posts

fun managePostTags(pb: BosBase, postId: String, newTagId: String, tagToRemove: String) {
    // Add new tag, remove old tag
    pb.collection("posts").update(
        id = postId,
        body = mapOf(
            "tags+" to newTagId,      // Append new tag
            "tags-" to tagToRemove    // Remove old tag
        )
    )
}

User Profile with All Posts

fun getUserProfile(pb: BosBase, userId: String) {
    // Get user with all their posts (back-relation)
    val user = pb.collection("users").getOne(
        id = userId,
        expand = "posts_via_author"
    )
    
    println("User: ${user["name"]?.jsonPrimitive?.contentOrNull}")
    
    // Access back-relation
    val posts = user["expand"]?.jsonObject?.get("posts_via_author")?.jsonArray
    println("Posts count: ${posts?.size ?: 0}")
    
    posts?.forEach { post ->
        val title = post.jsonObject["title"]?.jsonPrimitive?.contentOrNull
        println("  - $title")
    }
}