Relationships
Define associations between models using a simple DSL. SoliLang handles foreign keys, eager loading, and join filtering automatically.
Relationship DSL
Declare associations inside your model class using the built-in relationship methods:
| Method | Description |
|---|---|
has_many(name) |
Declare a one-to-many relationship |
has_one(name) |
Declare a one-to-one relationship |
belongs_to(name) |
Declare an inverse relationship |
belongs_to(name, { "polymorphic": true }) |
Declare a polymorphic relationship |
Defining Relationships
Add relationship declarations at the top of your model class body:
class User < Model
has_many("posts")
has_one("profile")
end
class Post < Model
belongs_to("user")
has_many("comments")
end
Naming Conventions
SoliLang automatically infers the related class, collection, and foreign key from the relationship name:
| Declaration | Related Class | Collection | Foreign Key |
|---|---|---|---|
has_many("posts") |
Post | posts | user_id |
has_one("profile") |
Profile | profiles | user_id |
belongs_to("user") |
User | users | user_id |
Custom Foreign Keys
Override the default naming conventions with an options hash:
class Post < Model
belongs_to("author", { "class_name": "User", "foreign_key": "author_id" })
end
Polymorphic Relationships
A polymorphic belongs_to allows a model to belong to more than one other model on a single association.
The related type and ID are stored in commentable_type and commentable_id fields:
class Comment < Model
belongs_to("commentable", { "polymorphic": true })
end
Relationship Accessors
Access related records directly from model instances. You can also chain query builder methods on has_many relations:
let user = User.find("user_id")
# Access has_many relation
let posts = user.posts
# Access has_one relation
let profile = user.profile
# Access belongs_to relation
let author = post.user
# Chain query builder methods on relations
let published = user.posts.where("published = @p", { "p": true }).all
Eager Loading (includes)
Without eager loading, accessing relations in a loop triggers a separate query for each record — the classic N+1 problem.
Use .includes() to preload related records in a single query via LET subqueries with MERGE:
# Load users with their posts and profiles in a single query
let users = User.includes("posts", "profile").all
# Combine with where clauses
let active = User.where("active = @a", { "a": true }).includes("posts").first
# Inspect the generated query
print(User.includes("posts").to_query)
has_many includes return an array. has_one and belongs_to includes return a single document (via FIRST()).
Join Filtering
Filter records by the existence of related records. Unlike includes, join does not preload the related data — it only filters the parent records:
# Find users who have at least one post
let users_with_posts = User.join("posts").all
# Find users who have published posts
let count = User.join("posts", "published = @p", { "p": true }).count
# Chain with other query methods
let recent = User.join("posts").order("created_at", "desc").limit(10).all
Filtered Includes
Filter included relations to load only matching related records:
# Only load published posts for each user
let users = User.includes("posts", "published = @p", { "p": true }).all
# Combine a filter with field projection using the "fields" key
let users = User.includes("posts", "published = @p", {
"p": true,
"fields": ["title", "body"]
}).all
Includes with Field Projection
Use a hash argument to select specific fields on included relations (without filtering):
# Only load title and body from posts
let users = User.includes({ "posts": ["title", "body"] }).all
Chaining Multiple Includes
Chain .includes() calls to eagerly load multiple relations with different options:
# Filtered posts + unfiltered profile
let users = User.includes("posts", "published = @p", { "p": true })
.includes("profile")
.all
Manual Relationships
For more control, implement relationships as custom instance methods:
class Post < Model
def author
User.find(this.author_id)
end
end