Documents
Documents are Spaceport's powerful ORM-like interface for interacting with your CouchDB database. They allow you to work with your data as native Groovy objects, abstracting away the complexities of JSON serialization and HTTP communication. This system enables you to define clear data models, encapsulate business logic directly within those models, and manage data persistence seamlessly.
The Document system is built on several interconnected components:
- Documents: The primary class for representing database records as objects
- CouchHandler: The bridge between Spaceport and CouchDB (accessible via
Spaceport.main_memory_core) - Operations: Result objects returned from database operations
- ViewDocuments: Special documents containing CouchDB Views for querying data
- Views: Query result sets from ViewDocuments
- Cargo Integration: Automatic synchronization between Cargo objects and document data
- Alerts Integration: Lifecycle hooks for document events
# Core Concepts
## The Document Class
The Document class is the foundation of Spaceport's data persistence layer. Every document has:
_id: A unique identifier within its database (managed by CouchDB)_rev: A revision identifier that changes with each save (for conflict detection)database: The CouchDB database where the document resides (not serialized)type: An optional string for categorizing documentsfields: A map for storing data exposed to clients (should be sanitized)cargo: A built-in Cargo object for data manipulation with reactivity supportupdates: A map tracking document lifecycle events by timestampstates: A map for managing document state history (advanced usage)_attachments: Storage for file attachments in base64 format
Documents can be used directly or extended to create custom document types with specific behavior and validation.
## CouchHandler and the Memory Core
CouchHandler is Spaceport's internal bridge to CouchDB. You typically interact with it through
Spaceport.main_memory_core, which provides:
- Database Management: Create, delete, and check for databases
- Document Operations: Fetch, create, update, and delete documents
- View Queries: Execute CouchDB views for data querying
- Attachment Handling: Manage file attachments on documents
- Caching: Automatic document caching for performance
While most document operations use the Document class methods, Spaceport.main_memory_core is useful for
database-level operations like checking if a database exists or creating new databases.
## Operations: Database Operation Results
Every database operation returns an Operation object that provides detailed feedback:
Operation op = document.save()
if (op.wasSuccessful()) {
println "Document saved with revision: ${op.rev}"
} else {
println "Save failed: ${op.error} - ${op.reason}"
}
Operation Properties:
| Property | Type | Description |
|---|---|---|
ok |
Boolean |
true if operation succeeded, false otherwise |
id |
String |
Document ID (for successful operations) |
rev |
String |
New revision ID (for save operations) |
reason |
String |
Human-readable message about the operation |
error |
String |
Error code or identifier (for failed operations) |
Helper Methods:
wasSuccessful(): Returnstrueif the operation succeeded
# Working with Documents
## Fetching Documents
The Document class provides several static methods for retrieving documents from the database.
### get(id, database): Get or Create
The most common method. It retrieves an existing document or creates a new one if it doesn't exist.
import spaceport.computer.memory.physical.Document
// Get or create a document with ID 'app-settings' in the 'configuration' database
def settings = Document.get('app-settings', 'configuration')
// The document is now available for manipulation
settings.fields.theme = 'dark'
settings.fields.language = 'en'
settings.save()
When a document is created, an on document created alert is fired with this context:
[ 'id': id, 'database': database, 'doc': doc, 'classType': Document.class ]
### getIfExists(id, database): Fetch Only
Returns the document if it exists, or null if it doesn't. Does not create a new document.
def article = Document.getIfExists('my-article', 'articles')
if (article) {
// Article exists, work with it
println "Title: ${article.fields.title}"
} else {
// Article doesn't exist, handle accordingly
println "Article not found"
}
### getUncached(id, database): Force Fresh Fetch
Bypasses the document cache to ensure you get the latest version from the database. Useful when you know another process may have modified the document.
// Get the freshest version, ignoring any cached copy
def doc = Document.getUncached('user-123', 'users')
### getNew(database): Create with Random ID
Creates a new document with a randomly generated UUID.
// Create a new session document with a unique ID
def session = Document.getNew('sessions')
// The ID is automatically generated
println "New session ID: ${session._id}"
### exists(id, database): Check Existence
Checks if a document exists without retrieving its full contents.
if (Document.exists('admin', 'users')) {
println "Admin user already exists"
} else {
// Create the admin user
}
## Manipulating Document Data
Documents provide several properties for storing different types of data.
### The fields Map
The fields property is the primary container for your document's data. It's a standard Groovy Map intended for
data that might be exposed to clients, so always sanitize user input.
def product = Document.get('product-123', 'products')
// Set fields directly
product.fields.name = 'Rocket Fuel'
product.fields.price = 1299.99
product.fields.inStock = true
product.fields.tags = ['fuel', 'premium', 'efficient']
// Access fields
def price = product.fields.price
def tags = product.fields.tags
// Save changes
product.save()
Best Practice: When storing user-submitted data, use Spaceport's .clean() method to sanitize HTML:
// Sanitize user input before storing
product.fields.description = userInput.clean()
### The cargo Property
Every document has a built-in Cargo object that provides advanced data manipulation features. Unlike fields,
cargo is designed for internal application data and offers powerful methods for counters, toggles, sets, and more.
def article = Document.get('article-456', 'articles')
// Use cargo for counters
article.cargo.inc('viewCount') // Increment view count
article.cargo.inc('likes', 5) // Increment by 5
// Use cargo for toggles
article.cargo.toggle('featured') // Toggle boolean value
// Use cargo for sets (unique lists)
article.cargo.addToSet('tags', 'groovy')
article.cargo.addToSet('tags', 'spaceport')
article.save()
See the Cargo Documentation for complete details on all available methods.
### The updates Map
The updates property automatically tracks document lifecycle events using timestamps as keys. Spaceport
automatically adds entries for document creation and modification, but you can add your own custom entries.
def order = Document.get('order-789', 'orders')
// Spaceport automatically tracks creation
// order.updates[someTimestamp] = 'created'
// Add custom lifecycle events
order.updates[System.currentTimeMillis()] = [
action: 'shipped',
carrier: 'Cosmic Express',
trackingNumber: 'CE123456789'
]
order.save()
// Later, review the history
order.updates.each { timestamp, event ->
println "${new Date(timestamp as Long)}: ${event}"
}
### The type Property
Setting a type on your documents is a best practice for categorizing and querying different kinds of data within
the same database.
def invoice = Document.get('inv-001', 'documents')
invoice.type = 'invoice'
invoice.fields.amount = 5000.00
invoice.save()
def receipt = Document.get('rec-001', 'documents')
receipt.type = 'receipt'
receipt.fields.amount = 5000.00
receipt.save()
// Later, you can query by type using Views
## Saving and Removing Documents
### save(): Persist Changes
The save() method persists all changes to the database and returns an Operation object.
def user = Document.get('user-alice', 'users')
user.fields.email = 'alice@example.com'
user.fields.lastLogin = System.currentTimeMillis()
// Save returns an Operation
Operation result = user.save()
if (result.wasSuccessful()) {
println "Saved successfully. New revision: ${result.rev}"
// The document's _rev is automatically updated
} else {
println "Save failed: ${result.reason}"
}
Important Notes:
- The document's
_revis automatically updated after a successful save - Spaceport uses
_revfor conflict detection (optimistic locking) - Several alerts are fired during the save process (see Document Lifecycle Alerts)
### Conflict Detection and Resolution
CouchDB uses revision IDs (_rev) to detect conflicts. If two processes try to update the same document
simultaneously, the second save will fail with a conflict error.
// Process A fetches the document
def docA = Document.get('shared-doc', 'data')
docA.fields.value = 'A'
// Process B fetches the same document
def docB = Document.get('shared-doc', 'data')
docB.fields.value = 'B'
// Process A saves first (succeeds)
docA.save()
// Process B tries to save (conflict!)
Operation result = docB.save()
if (!result.wasSuccessful() && result.error == 'conflict') {
// Handle the conflict
// Option 1: Fetch fresh copy and retry
docB = Document.getUncached('shared-doc', 'data')
docB.fields.value = 'B'
docB.save()
}
Spaceport fires an on document conflict alert during save operations when a conflict is detected, allowing you to
implement custom conflict resolution strategies. See Handling Conflicts with Alerts.
### remove(): Delete Document
The remove() method permanently deletes a document from the database.
def tempDoc = Document.get('temp-123', 'temporary')
// Delete the document
Operation result = tempDoc.remove()
if (result.wasSuccessful()) {
println "Document removed successfully"
}
Alerts Fired:
on document remove- Before deletion (can be used to prevent deletion)on document removed- After successful deletionon document modified- After any modification (withaction: 'removed')
### close(): Remove from Cache
The close() method removes a document from Spaceport's internal cache. The next time the document is requested,
it will be fetched fresh from the database.
def doc = Document.get('cached-doc', 'data')
doc.fields.value = 'modified'
// Remove from cache without saving
doc.close()
// Next fetch will get the original, unmodified version from the database
def fresh = Document.get('cached-doc', 'data')
// fresh.fields.value is still the old value
Use Case: This is useful when you've made changes you want to discard, or when you know another process has updated the document and you want to ensure you get the latest version.
# Custom Document Classes
Extending the Document class allows you to create strongly-typed data models with custom behavior, validation,
and business logic.
## Creating a Custom Document Class
Here's a complete example of a custom document class for managing articles:
package documents
import spaceport.Spaceport
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
import spaceport.computer.memory.physical.Document
class ArticleDocument extends Document {
// Ensure the articles database exists at startup
@Alert('on initialize')
static void _init(Result r) {
if (!Spaceport.main_memory_core.containsDatabase('articles'))
Spaceport.main_memory_core.createDatabase('articles')
}
// Custom static method for fetching by ID
static ArticleDocument fetchById(String id) {
return getIfExists(id, 'articles') as ArticleDocument
}
// Custom static method for creating new articles
static ArticleDocument createNew(String title, String author) {
def article = getNew('articles') as ArticleDocument
article.type = 'article'
article.title = title
article.author = author
article.published = false
return article
}
// Tell Spaceport which additional properties to serialize
def customProperties = ['title', 'author', 'body', 'tags', 'published', 'publishedDate']
// Custom properties with getters and setters
String title = 'Untitled'
void setTitle(String title) {
// Automatically sanitize titles
this.title = title.clean()
}
String author = 'Anonymous'
String body = ''
void setBody(String body) {
// Allow basic HTML in body content
body.cleanType = 'simple'
this.body = body.clean()
}
List<String> tags = []
void addTag(String tag) {
if (!tags.contains(tag)) {
tags << tag.clean()
}
}
void removeTag(String tag) {
tags.remove(tag)
}
boolean published = false
Long publishedDate = null
void publish() {
this.published = true
this.publishedDate = System.currentTimeMillis()
this.save()
}
void unpublish() {
this.published = false
this.publishedDate = null
this.save()
}
// Custom business logic methods
boolean isPublished() {
return published && publishedDate != null
}
String getPublishedDateFormatted() {
if (publishedDate) {
return new Date(publishedDate).format('MMMM dd, yyyy')
}
return 'Not published'
}
int getWordCount() {
return body.split(/\s+/).size()
}
}
## Using Custom Document Classes
Once defined, custom document classes work just like the base Document class:
import documents.ArticleDocument
// Create a new article
def article = ArticleDocument.createNew(
'Building with Spaceport',
'Jeremy'
)
article.body = 'Spaceport is a comprehensive full-stack framework...'
article.addTag('spaceport')
article.addTag('groovy')
article.save()
// Fetch an existing article
def existing = ArticleDocument.fetchById('article-123')
if (existing) {
println "Title: ${existing.title}"
println "Author: ${existing.author}"
println "Word Count: ${existing.wordCount}"
println "Published: ${existing.publishedDateFormatted}"
// Publish the article
if (!existing.isPublished()) {
existing.publish()
}
}
## The customProperties List
The customProperties list tells Spaceport which additional properties should be serialized to the database. Without
this, only the standard Document properties (type, fields, cargo, updates, etc.) would be saved.
class MyDocument extends Document {
// These properties will be saved
def customProperties = ['title', 'author', 'tags']
String title
String author
List tags
// This property will NOT be saved (not in customProperties)
@JsonIgnore
transient String tempCalculation
}
## Type Casting and Conversion
You can convert documents between types using Groovy's as operator:
// Fetch a generic document
def doc = Document.get('my-article', 'articles')
// Convert to specific type
def article = doc as ArticleDocument
// Or fetch directly as a specific type
def article2 = ArticleDocument.getIfExists('my-article', 'articles')
The asType() method handles type conversion and checks Spaceport's cache for existing typed instances.
# Views and ViewDocuments
CouchDB Views are a powerful way to query and aggregate data. Spaceport provides ViewDocument and View classes
to work with them.
## Understanding CouchDB Views
Views are stored JavaScript functions in special _design documents. They consist of:
- Map Function: Processes each document and emits key-value pairs
- Reduce Function (optional): Aggregates the emitted values
Views are indexed automatically and updated incrementally, making them extremely efficient for querying.
## Creating a ViewDocument
ViewDocument is a special document type that contains View definitions.
import spaceport.computer.memory.physical.ViewDocument
// Get or create a ViewDocument named 'articles'
// (stored as '_design/articles' in CouchDB)
def viewDoc = ViewDocument.get('articles', 'articles')
// Define a view that finds all published articles
viewDoc.setView('by-published', '''
function(doc) {
if (doc.type === 'article' && doc.published === true) {
emit(doc.publishedDate, {
title: doc.title,
author: doc.author
});
}
}
''')
// Define a view that counts articles by author
viewDoc.setView('count-by-author',
'''
function(doc) {
if (doc.type === 'article') {
emit(doc.author, 1);
}
}
''',
'_count' // Built-in CouchDB reduce function
)
### setView() and setViewIfNeeded()
Both methods define or update a view, but with different save behavior:
setView(id, map, reduce, save): Always updates the view and saves by defaultsetViewIfNeeded(id, map): Only saves if the view definition has changed
// Always update and save
viewDoc.setView('my-view', mapFunction)
// Only save if changed (more efficient for startup scripts)
viewDoc.setViewIfNeeded('my-view', mapFunction)
// Chain multiple views before saving
viewDoc
.setView('view-one', mapFunction1, null, false)
.setView('view-two', mapFunction2, null, false)
.setView('view-three', mapFunction3, null, true) // Save on last one
## Querying Views
Once a view is defined, you can query it to retrieve results.
import spaceport.computer.memory.physical.View
// Get view results
def view = View.get('articles', 'by-published', 'articles')
// Access the rows
view.rows.each { row ->
println "Published: ${row.key} - Title: ${row.value.title}"
}
// Get total count
println "Total published articles: ${view.total_rows}"
### Query Parameters
CouchDB supports many query parameters to filter and control view results:
// Get view with parameters
def view = View.get('articles', 'by-author', 'articles', [
key: '"Jeremy"', // Only articles by Jeremy
limit: 10, // First 10 results
descending: true, // Reverse order
skip: 5, // Skip first 5
include_docs: true // Include full documents
])
### Including Documents
By default, view results only include keys and values. To get the full documents:
// Get view with documents included
def view = View.getWithDocuments('articles', 'by-published', 'articles')
// Access the documents
List<Document> docs = view.getDocuments()
docs.each { doc ->
println "Title: ${doc.fields.title}"
println "Body: ${doc.fields.body}"
}
The getDocuments() method automatically deserializes the included documents into Document objects.
## View Properties
A View object contains:
| Property | Type | Description |
|---|---|---|
rows |
List |
Array of result rows, each with key, value, and optionally doc |
total_rows |
Integer |
Total number of rows in the view (ignoring limit/skip) |
offset |
Integer |
Number of rows skipped |
database |
String |
The database this view is from (not serialized) |
## Practical View Examples
### Finding Documents by Type
def viewDoc = ViewDocument.get('all', 'mydb')
viewDoc.setView('by-type', '''
function(doc) {
if (doc.type) {
emit(doc.type, null);
}
}
''')
// Query for all 'user' documents
def users = View.get('all', 'by-type', 'mydb', [key: '"user"'])
### Aggregating Data with Reduce
def viewDoc = ViewDocument.get('orders', 'sales')
viewDoc.setView('total-by-month',
'''
function(doc) {
if (doc.type === 'order' && doc.date) {
var date = new Date(doc.date);
var month = date.getFullYear() + '-' + (date.getMonth() + 1);
emit(month, doc.total);
}
}
''',
'_sum' // Sum all order totals by month
)
// Get totals
def view = View.get('orders', 'total-by-month', 'sales', [group: true])
view.rows.each { row ->
println "Month: ${row.key}, Total: \$${row.value}"
}
### Complex Keys for Sorting
viewDoc.setView('by-status-and-date', '''
function(doc) {
if (doc.type === 'ticket') {
emit([doc.status, doc.created], {
title: doc.title,
assignee: doc.assignee
});
}
}
''')
// Get all 'open' tickets sorted by creation date
def openTickets = View.get('tickets', 'by-status-and-date', 'support', [
startkey: '["open"]',
endkey: '["open", {}]'
])
# Cargo Integration
Documents have deep integration with Spaceport's Cargo system, providing automatic persistence and reactivity.
## Built-in Document Cargo
Every document has a cargo property that works like any other Cargo object:
def doc = Document.get('stats', 'data')
// Use cargo methods
doc.cargo.set('hitCount', 0)
doc.cargo.inc('hitCount')
doc.cargo.toggle('active')
doc.cargo.addToSet('tags', 'important')
// Save the document to persist cargo changes
doc.save()
## Mirrored Cargo with Auto-Save
For automatic persistence, use Cargo.fromDocument() to create a mirrored Cargo that automatically saves changes:
import spaceport.computer.memory.virtual.Cargo
import spaceport.computer.memory.physical.Document
def doc = Document.get('article-meta', 'articles')
// Create a mirrored Cargo
def meta = Cargo.fromDocument(doc)
// Any changes are automatically saved asynchronously
meta.inc('viewCount') // Auto-saves!
meta.addToSet('categories', 'tech') // Auto-saves!
meta.set('featured', true) // Auto-saves!
// No need to call doc.save() manually
See the Cargo documentation for complete details on mirrored Cargo objects.
## Reactive UIs with Document Cargo
Mirrored Cargo objects integrate with Launchpad's reactive system for real-time UI updates:
<%
def article = ArticleDocument.fetchById('my-article')
def meta = Cargo.fromDocument(article)
%>
<div class="article-stats">
<span>Views: ${{ meta.getInteger('viewCount') }}</span>
<button on-click=${ _{ meta.inc('likes') }}>
Like (${{ meta.getInteger('likes') }})
</button>
</div>
When a user clicks the Like button, the count increments on the server, auto-saves to the database, and the UI updates in real-time.
# Document Lifecycle Alerts
Spaceport fires several alerts during document operations, allowing you to implement hooks for auditing, validation, cache invalidation, and more.
## Available Document Alerts
| Alert String | Timing | Context Properties | Description |
|---|---|---|---|
on document created |
After creation | id, database, doc, classType |
A new document was created |
on document save |
Before save | type, old, document, difference |
Before document is saved (can modify) |
on document saved |
After save (async) | type, old, document, difference, operation |
After successful save |
on document modified |
After any change | type, action, document, operation, difference, old |
After any modification |
on document conflict |
During save conflict | type, document, conflicted, changes, differences |
Conflict detected during save |
on document remove |
Before deletion | type, document |
Before document is deleted |
on document removed |
After deletion | type, document, operation |
After successful deletion |
## Using Document Alerts
Here are practical examples of using document lifecycle alerts:
### Audit Logging
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
import spaceport.computer.memory.physical.Document
class AuditLogger {
@Alert('on document modified')
static _logChange(Result r) {
def auditEntry = Document.getNew('audit-log')
auditEntry.type = 'audit'
auditEntry.fields.documentId = r.context.document._id
auditEntry.fields.documentType = r.context.type
auditEntry.fields.action = r.context.action // 'saved' or 'removed'
auditEntry.fields.timestamp = System.currentTimeMillis()
auditEntry.fields.changes = r.context.difference
auditEntry.save()
}
}
### Cache Invalidation
class CacheManager {
@Alert('on document saved')
static _invalidateCache(Result r) {
def doc = r.context.document
// Clear cache entries for this document
Cache.remove("doc:${doc._id}")
// Clear related caches
if (doc.type == 'article') {
Cache.remove('articles:recent')
Cache.remove('articles:by-author')
}
}
}
### Validation Before Save
class Validator {
@Alert('on document save')
static _validateArticle(Result r) {
def doc = r.context.document
if (doc.type == 'article') {
// Validate required fields
if (!doc.title || doc.title.trim().isEmpty()) {
doc.title = 'Untitled'
}
if (!doc.author) {
doc.author = 'Anonymous'
}
// Ensure tags is a list
if (!(doc.tags instanceof List)) {
doc.tags = []
}
}
}
}
### Preventing Deletion
class DeletionGuard {
@Alert(value = 'on document remove', priority = 100)
static _preventCriticalDeletion(Result r) {
def doc = r.context.document
// Prevent deletion of protected documents
if (doc.fields?.protected == true) {
println "Attempted to delete protected document: ${doc._id}"
r.cancelled = true // Stop the deletion
}
}
}
## Handling Conflicts with Alerts
The on document conflict alert fires during save operations when CouchDB detects a conflict. This allows you to
implement custom conflict resolution strategies:
class ConflictResolver {
@Alert('on document conflict')
static _resolveConflict(Result r) {
def current = r.context.document // Your version
def conflicted = r.context.conflicted // Database version
def changes = r.context.changes // Your changes
def differences = r.context.differences // What's different
// Strategy 1: Always keep our changes (overwrite theirs)
current._rev = conflicted._rev
// Changes will be applied on retry
// Strategy 2: Merge changes intelligently
if (current.type == 'counter') {
// For counters, add the values instead of replacing
def ourCount = current.fields.count
def theirCount = conflicted.fields.count
def originalCount = ourCount - (changes.count?.new ?: 0)
current.fields.count = originalCount + (theirCount - originalCount) + (changes.count?.new ?: 0)
current._rev = conflicted._rev
}
// Strategy 3: Take their version and discard ours
// (Just let the save fail, and fetch fresh copy)
}
}
# Document State Management
Documents include a states property for tracking state history over time. This is an advanced feature for
workflows and state machines.
## Setting and Getting State
def order = Document.get('order-123', 'orders')
// Set a state with value and details
order.setState('fulfillment', 'processing', [
warehouse: 'West Coast',
estimatedShip: '2025-11-01'
])
// Or just a simple value
order.setState('payment', 'paid')
// Get current state
def fulfillmentState = order.getState('fulfillment')
println "Status: ${fulfillmentState.value}"
println "Details: ${fulfillmentState.details}"
// Get state history
def history = order.getStateHistory('fulfillment')
history.each { timestamp, state ->
println "At ${new Date(timestamp as Long)}: ${state.value}"
}
order.save()
## State with Closures
You can use closures to update state based on previous state:
order.setState('progress') { previousState ->
def currentPercent = previousState.value ?: 0
return [
value: currentPercent + 25,
details: [step: 'Next stage complete']
]
}
# Database Management
While most operations use the Document class, you can access Spaceport.main_memory_core (a CouchHandler instance)
directly for database-level operations.
## Creating and Deleting Databases
import spaceport.Spaceport
// Check if database exists
if (!Spaceport.main_memory_core.containsDatabase('mydb')) {
// Create database
def op = Spaceport.main_memory_core.createDatabase('mydb')
if (op.wasSuccessful()) {
println "Database 'mydb' created"
}
}
// Delete a database (careful!)
def op = Spaceport.main_memory_core.deleteDatabase('tempdb')
if (op.wasSuccessful()) {
println "Database 'tempdb' deleted"
}
## Querying All Documents
// Get all document IDs in a database
def view = Spaceport.main_memory_core.getAll('mydb')
view.rows.each { row ->
println "Document ID: ${row.id}"
}
// Get all documents with their contents
def view = Spaceport.main_memory_core.getAll('mydb', [include_docs: true])
def docs = view.getDocuments()
docs.each { doc ->
println "Document: ${doc._id}, Type: ${doc.type}"
}
# Attachments
Documents can have file attachments stored in the _attachments property. Attachments are stored in base64 format
for inline attachments, or can be fetched as streams for large files.
## Working with Attachments
// Get an attachment
def outputStream = new ByteArrayOutputStream()
def metadata = Spaceport.main_memory_core.getDocAttachment(
'profile-pic.jpg', // Attachment name
'user-123', // Document ID
'users', // Database
outputStream // Where to write the data
)
println "Content type: ${metadata.content_type}"
println "Content length: ${metadata.content_length}"
// The attachment data is now in outputStream
byte[] imageData = outputStream.toByteArray()
# Performance and Caching
## Document Caching
Spaceport automatically caches documents for performance. The cache key is "${_id}/${database}".
// First fetch - goes to database
def doc1 = Document.get('my-doc', 'mydb')
// Second fetch - returns cached copy (instant)
def doc2 = Document.get('my-doc', 'mydb')
// doc1 and doc2 are the same object instance
assert doc1.is(doc2)
// Force a fresh fetch
def doc3 = Document.getUncached('my-doc', 'mydb')
// Manually clear from cache
doc1.close()
Cache Behavior:
- Caching is automatic and transparent
- Custom document types are cached correctly
- Cache is thread-safe (uses
ConcurrentHashMap) - Documents are automatically removed from cache after
remove()
## When to Use getUncached()
Use getUncached() when:
- Another process may have modified the document
- You're implementing a polling mechanism
- You need to ensure you have the absolute latest version
- You're debugging cache-related issues
For most use cases, the standard get() method's caching provides better performance without issues.
# Best Practices
## General Guidelines
1. Always set a type property for documents to enable easy querying and filtering
2. Use fields for client-exposed data and sanitize user input with .clean()
3. Use cargo for internal application data and leverage its rich methods
4. Track important events in the updates map with timestamps
5. Create custom document classes for complex data models with behavior
6. Use Views instead of loading all documents and filtering in code
7. Leverage Alerts for cross-cutting concerns like auditing and validation
## Data Organization
// Good: Clear, structured data
def user = Document.get('user-123', 'users')
user.type = 'user'
user.fields.username = 'alice'
user.fields.email = 'alice@example.com'
user.cargo.inc('loginCount')
user.updates[System.currentTimeMillis()] = 'created'
// Avoid: Mixing concerns, unclear structure
def user = Document.get('user-123', 'users')
user.fields.data = [
user: 'alice',
info: ['a@e.com', 0],
misc: [:]
]
## Custom Document Classes
// Good: Clear responsibilities, encapsulated logic
class UserDocument extends Document {
def customProperties = ['username', 'email', 'role']
String username
String email
String role = 'user'
boolean isAdmin() {
return role == 'admin'
}
void promoteToAdmin() {
this.role = 'admin'
this.save()
}
}
// Avoid: Accessing fields directly, no encapsulation
def user = Document.get('user', 'users')
if (user.fields.role == 'admin') {
// Complex logic without helper methods
}
## Error Handling
// Good: Check operation results
def doc = Document.get('data', 'mydb')
doc.fields.value = 'new value'
def result = doc.save()
if (!result.wasSuccessful()) {
println "Save failed: ${result.error} - ${result.reason}"
if (result.error == 'conflict') {
// Handle conflict
doc = Document.getUncached('data', 'mydb')
doc.fields.value = 'new value'
doc.save()
}
}
// Avoid: Ignoring operation results
doc.save() // Did it work? Who knows!
# Common Patterns
## Singleton Configuration Documents
class AppSettings {
private static settings = Document.get('app-settings', 'config')
static String get(String key) {
return settings.fields[key]
}
static void set(String key, value) {
settings.fields[key] = value
settings.save()
}
}
// Usage
AppSettings.set('theme', 'dark')
def theme = AppSettings.get('theme')
## Document Collections with Views
class Articles {
static {
// Setup views at startup
def viewDoc = ViewDocument.get('articles', 'articles')
viewDoc.setViewIfNeeded('published', '''
function(doc) {
if (doc.type === 'article' && doc.published) {
emit(doc.publishedDate, null);
}
}
''')
}
static List<ArticleDocument> getPublished(int limit = 10) {
def view = View.getWithDocuments('articles', 'published', 'articles', [
limit: limit,
descending: true
])
return view.getDocuments().collect { it as ArticleDocument }
}
static ArticleDocument create(String title, String author) {
def article = ArticleDocument.getNew('articles')
article.type = 'article'
article.title = title
article.author = author
article.save()
return article
}
}
## Soft Deletion
class SoftDeletable extends Document {
def customProperties = ['deleted', 'deletedAt']
boolean deleted = false
Long deletedAt = null
void softDelete() {
this.deleted = true
this.deletedAt = System.currentTimeMillis()
this.save()
}
void restore() {
this.deleted = false
this.deletedAt = null
this.save()
}
}
// Setup a view to exclude deleted documents
viewDoc.setView('active', '''
function(doc) {
if (doc.type === 'user' && !doc.deleted) {
emit(doc._id, null);
}
}
''')
# Troubleshooting
## Common Issues
Document Not Found:
def doc = Document.getIfExists('unknown', 'mydb')
if (!doc) {
println "Document doesn't exist - use get() to create it"
doc = Document.get('unknown', 'mydb')
}
Conflict Errors:
// Fetch the latest version and retry
doc = Document.getUncached(doc._id, doc.database)
doc.fields.value = newValue
doc.save()
Custom Properties Not Saving:
// Make sure to define customProperties in your class
class MyDoc extends Document {
def customProperties = ['myField'] // Required!
String myField
}
Cache Issues:
// Clear the cache if you suspect stale data
doc.close()
// Or force an uncached fetch
doc = Document.getUncached(id, database)
# Summary
The Document system provides a comprehensive ORM-like interface for CouchDB with:
- Simple API: Get, create, save, and remove documents with ease
- Custom Classes: Extend Document for strongly-typed models with business logic
- Powerful Querying: Use Views for efficient data retrieval and aggregation
- Automatic Features: Built-in caching, conflict detection, and lifecycle tracking
- Deep Integration: Seamless connection with Cargo for reactivity and Alerts for hooks
- Flexible Storage: Multiple properties (
fields,cargo,updates,states) for different needs
# Next Steps
Now that you understand Documents, explore these related topics:
- Cargo: Deep dive into the Cargo system for data manipulation and reactivity
- Alerts: Master the event system for document lifecycle hooks
- Launchpad: Build reactive UIs that respond to document changes in real-time
- Source Modules: Organize your document classes and application logic
SPACEPORT DOCS