Skip to content

Commit fcfa3b6

Browse files
committed
Refactor documentation to replace 'includes' with 'preload' for eager loading examples
1 parent e36dda5 commit fcfa3b6

16 files changed

Lines changed: 250 additions & 40 deletions

File tree

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -346,11 +346,11 @@ user.posts.create(title: "New Post", content: "Content")
346346
user.posts.size # Efficient count without loading all records
347347
user.posts.any? # Check existence without memory overhead
348348
349-
# Smart eager loading
350-
users_with_posts = User.includes(:posts, :profile)
349+
# Eager loading with preload (avoids N+1 queries)
350+
users_with_posts = User.preload(:posts)
351351
.where(active: true)
352352
.all
353-
# Single query instead of N+1 queries!
353+
# Only 2 queries instead of N+1 queries!
354354
```
355355

356356
### Transaction Management
@@ -486,10 +486,10 @@ fragment_cache = CQL::Cache::FragmentCache.new(memory_cache)
486486
# Intelligent caching with automatic invalidation
487487
result = fragment_cache.cache_fragment("expensive_query", {"user_id" => user.id}) do
488488
# Expensive database operation cached automatically
489-
User.joins(:posts, :comments)
489+
User.join(:posts)
490490
.where(active: true)
491-
.includes(:profile)
492-
.complex_aggregation
491+
.preload(:profile)
492+
.all
493493
end
494494
495495
# Tag-based cache invalidation
@@ -516,7 +516,7 @@ detector.analyze_queries(queries) do |pattern|
516516
puts "⚠️ N+1 Query Pattern Detected:"
517517
puts " Model: #{pattern.model}"
518518
puts " Association: #{pattern.association}"
519-
puts " Suggestion: Use .includes(:#{pattern.association})"
519+
puts " Suggestion: Use .preload(:#{pattern.association})"
520520
end
521521
522522
# Generate beautiful performance reports

docs/advanced-topics/performance-optimization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ users.each do |user|
5050
end
5151
5252
# Good: Eager loading
53-
users = User.includes(:posts).all
53+
users = User.preload(:posts).all
5454
users.each do |user|
5555
puts user.posts.size # No additional queries
5656
end

docs/advanced-topics/performance-tools.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ posts.each do |post|
104104
end
105105
106106
# Better approach:
107-
posts = Post.includes(:user).all # Single query with JOIN
107+
posts = Post.preload(:user).all # Single query with JOIN
108108
posts.each do |post|
109109
puts "#{post.title} by #{post.user.name}" # No additional queries
110110
end
@@ -434,7 +434,7 @@ end
434434
435435
# Usage
436436
users = benchmark_query("User.all with includes") do
437-
User.includes(:posts, :comments).all
437+
User.preload(:posts, :comments).all
438438
end
439439
```
440440

docs/caching-and-performance/caching-guide.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ def render_category_tree
314314
tags: ["categories"],
315315
ttl: 30.minutes
316316
) do
317-
Category.root_categories.includes(:children).to_json
317+
Category.root_categories.preload(:children).to_json
318318
end
319319
end
320320
```
@@ -451,7 +451,7 @@ class ProductsController < ApplicationController
451451
452452
product_json = cache_fragment("product_#{product_id}") do
453453
product = Product.find(product_id)
454-
reviews = product.reviews.includes(:user).limit(10)
454+
reviews = product.reviews.preload(:user).limit(10)
455455
456456
{
457457
product: product.to_json,

docs/caching-and-performance/per-request-query-caching.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,11 @@ class BlogApp < Azu::Application
308308
309309
get "/articles" do |ctx|
310310
# First query - hits database
311-
articles = Article.published.includes(:author)
311+
articles = Article.published.preload(:author)
312312
313313
# If this same query runs again in the same request
314314
# (e.g., in a partial or helper), it hits the cache
315-
popular_articles = Article.published.includes(:author)
315+
popular_articles = Article.published.preload(:author)
316316
317317
render_template "articles/index.html", {
318318
articles: articles,
@@ -359,8 +359,8 @@ class UsersController < ApplicationController
359359
360360
def dashboard
361361
# Complex queries that benefit from caching
362-
@active_users = User.where(active: true).includes(:posts)
363-
@recent_posts = Post.recent.includes(:author)
362+
@active_users = User.where(active: true).preload(:posts)
363+
@recent_posts = Post.recent.preload(:author)
364364
365365
# If these queries are repeated (e.g., in partials), they hit cache
366366
render "dashboard"

docs/examples-and-tutorials/examples-1/generated-schema-example.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,12 @@ comment.save!
310310
puts "Created comment: #{comment.content} (ID: #{comment.id})"
311311
312312
# Query with relationships
313-
posts = Post.includes(:user).all
313+
posts = Post.preload(:user).all
314314
posts.each do |post|
315315
puts "Post: #{post.title} by #{post.user.try(&.name) || 'Unknown'}"
316316
end
317317
318-
comments = Comment.includes(:user, :post).all
318+
comments = Comment.preload(:user, :post).all
319319
comments.each do |comment|
320320
puts "Comment: #{comment.content} by #{comment.user.try(&.name) || 'Unknown'} on #{comment.post.try(&.title) || 'Unknown Post'}"
321321
end

docs/examples/generated-schema-example.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,12 @@ comment.save!
310310
puts "Created comment: #{comment.content} (ID: #{comment.id})"
311311
312312
# Query with relationships
313-
posts = Post.includes(:user).all
313+
posts = Post.preload(:user).all
314314
posts.each do |post|
315315
puts "Post: #{post.title} by #{post.user.try(&.name) || 'Unknown'}"
316316
end
317317
318-
comments = Comment.includes(:user, :post).all
318+
comments = Comment.preload(:user, :post).all
319319
comments.each do |comment|
320320
puts "Comment: #{comment.content} by #{comment.user.try(&.name) || 'Unknown'} on #{comment.post.try(&.title) || 'Unknown Post'}"
321321
end

docs/foundation/crud-operations/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,9 @@ users = User.query
385385
.where(active: true)
386386
.limit(100)
387387
388-
# Use includes for eager loading (prevents N+1)
388+
# Use preload for eager loading (prevents N+1)
389389
posts = Post.query
390-
.includes(:user, :comments)
390+
.preload(:user, :comments)
391391
.where(published: true)
392392
393393
# Use batch operations for large datasets

docs/guides/active-record-with-cql/queryable.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,96 @@ end
763763

764764
---
765765

766+
## Eager Loading with Preload
767+
768+
Eager loading allows you to load associated records in batch queries, avoiding the N+1 query problem. CQL provides the `preload` method for efficient association loading.
769+
770+
### Basic Preloading
771+
772+
```crystal
773+
# Preload a single association
774+
users = User.preload(:posts).all
775+
users.each do |user|
776+
# posts are already loaded - no additional query
777+
puts "#{user.name} has #{user.posts.size} posts"
778+
end
779+
780+
# Preload multiple associations
781+
users = User.preload(:posts, :comments).all
782+
users.each do |user|
783+
puts "#{user.name}: #{user.posts.size} posts, #{user.comments.size} comments"
784+
end
785+
```
786+
787+
### Chaining with Where
788+
789+
Preload works seamlessly with other query methods:
790+
791+
```crystal
792+
# Filter users then preload their posts
793+
active_users = User.where(active: true)
794+
.preload(:posts)
795+
.order(:name)
796+
.all
797+
798+
# Only 2 queries total:
799+
# 1. SELECT * FROM users WHERE active = true ORDER BY name
800+
# 2. SELECT * FROM posts WHERE user_id IN (...)
801+
```
802+
803+
### How Preload Works
804+
805+
Unlike JOIN-based eager loading, `preload` uses separate queries:
806+
807+
1. First query fetches the primary records
808+
2. Subsequent queries fetch associated records using `IN` clauses
809+
3. Records are matched in memory and associations are populated
810+
811+
This approach:
812+
813+
- Avoids cartesian product issues with multiple has_many associations
814+
- Keeps queries simple and indexable
815+
- Works well with pagination
816+
817+
### Preload vs Join
818+
819+
| Aspect | `preload` | `join` |
820+
|--------------------------|------------------------------|---------------------------------|
821+
| Query count | N+1 reduced to 2 queries | 1 query |
822+
| Cartesian product | No | Yes (with multiple has_many) |
823+
| Filtering on association | No | Yes |
824+
| Pagination | Works correctly | May return duplicates |
825+
| Best for | Loading data to display | Filtering by association |
826+
827+
```crystal
828+
# Use preload when you need the associated data
829+
users = User.preload(:posts).all
830+
831+
# Use join when filtering by association
832+
users_with_recent_posts = User.join(:posts)
833+
.where("posts.created_at > ?", 1.week.ago)
834+
.distinct
835+
.all
836+
```
837+
838+
### Performance Considerations
839+
840+
```crystal
841+
# Preload only what you need
842+
users = User.preload(:posts).all # Good - loads posts in one query
843+
844+
# Avoid preloading unused associations
845+
users = User.preload(:posts, :comments, :profile).all # Wasteful if you only use posts
846+
847+
# Combine with select for optimal performance
848+
users = User.select(:id, :name)
849+
.preload(:posts)
850+
.where(active: true)
851+
.all
852+
```
853+
854+
---
855+
766856
## Advanced Query Composition
767857

768858
CQL's query interface allows you to build complex queries by combining multiple query methods. This section demonstrates common patterns and best practices for composing sophisticated queries.

docs/guides/active-record-with-cql/relations/README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,38 @@ Each guide includes:
164164
- **Advanced Features**: Using collection methods, dependent options, and performance optimizations
165165
- **Complete Examples**: Real-world scenarios with full code samples
166166

167+
## Eager Loading with Preload
168+
169+
CQL provides the `preload` method to efficiently load associations and avoid N+1 query problems:
170+
171+
```crystal
172+
# Load users with their posts in 2 queries (instead of N+1)
173+
users = User.preload(:posts).all
174+
users.each do |user|
175+
puts "#{user.name} has #{user.posts.size} posts" # No additional query
176+
end
177+
178+
# Preload multiple associations
179+
users = User.preload(:posts, :comments, :profile).all
180+
181+
# Chain with other query methods
182+
active_users = User.where(active: true)
183+
.preload(:posts)
184+
.order(:name)
185+
.all
186+
```
187+
188+
For more details, see:
189+
190+
- [Eager Loading in Queryable](../queryable.md#eager-loading-with-preload)
191+
- [HasMany Eager Loading](./hasmany.md#eager-loading)
192+
167193
## Best Practices
168194

169195
### Performance
170196

171197
- Use `cache: true` for frequently accessed `belongs_to` associations
172-
- Consider eager loading for scenarios where you'll access multiple associations
198+
- Use `preload` to avoid N+1 queries when accessing associations in loops
173199
- Use `dependent: :destroy` or `:delete` to maintain referential integrity
174200

175201
### Schema Design

0 commit comments

Comments
 (0)