Skip to content

Instantly share code, notes, and snippets.

@wware
Last active August 20, 2025 19:25
Show Gist options
  • Save wware/45e6b4bbe15b48467de4024f56464ad7 to your computer and use it in GitHub Desktop.
Save wware/45e6b4bbe15b48467de4024f56464ad7 to your computer and use it in GitHub Desktop.
Learn jimmer
.gradle/
target/
# btw the java files go in src/main/java/com/example/*.java

Marten Exploration Summary

What is Marten?

Marten is a document database library for .NET that uses PostgreSQL as its storage engine. It provides a NoSQL-like API while leveraging PostgreSQL's JSONB capabilities and ACID compliance.

Key Characteristics:

  • Stores .NET objects as JSON documents in PostgreSQL JSONB columns
  • Provides LINQ query support that translates to PostgreSQL JSON operators
  • Supports event sourcing patterns
  • Uses optimistic concurrency control
  • Handles serialization with opinionated defaults (camel-case properties, enum strings)

Both java files go in src/main/java/com/example/

Core Concepts

Document Storage Pattern

Instead of traditional relational tables, Marten stores entire objects as JSON documents:

-- Traditional relational approach
CREATE TABLE users (
    id UUID,
    name VARCHAR(100),
    email VARCHAR(100),
    created_at TIMESTAMP
);

-- Marten approach  
CREATE TABLE users (
    id UUID PRIMARY KEY,
    data JSONB NOT NULL  -- entire object stored here
);

LINQ to PostgreSQL JSON

Marten translates C# LINQ expressions into PostgreSQL JSON operators:

// C# LINQ
session.Query<User>()
    .Where(u => u.Email.Contains("@example.com"))
    .OrderBy(u => u.Name)

// Becomes PostgreSQL
SELECT data FROM users 
WHERE data->>'email' LIKE '%@example.com%' 
ORDER BY data->>'name'

PostgreSQL JSON Operators

  • ->> : Extract field as text (data->>'email')
  • -> : Extract field as JSON (data->'address')
  • @> : Contains (data @> '{"name": "Alice"}')
  • ? : Key exists (data ? 'email')

Working Example: Raw PostgreSQL JSONB

We built a simple example showing what Marten does under the hood:

// Store a document
Map<String, Object> user = new HashMap<>();
user.put("name", "Alice");
user.put("email", "[email protected]");

String userJson = mapper.writeValueAsString(user);
PreparedStatement insert = conn.prepareStatement(
    "INSERT INTO users (id, data) VALUES (?::uuid, ?::jsonb)");
insert.setString(1, UUID.randomUUID().toString());
insert.setString(2, userJson);

// Query with JSON operators
PreparedStatement query = conn.prepareStatement(
    "SELECT data FROM users WHERE data->>'email' LIKE ?");
query.setString(1, "%@example.com%");

Key insight: Marten eliminates this JDBC/JSON boilerplate with a clean object-oriented API.

Alternative: Jimmer (Java/Kotlin)

Jimmer provides similar functionality to Marten for the JVM ecosystem:

Entity Definition

@Entity
public interface User {
    @Id
    UUID id();
    String name();
    String email();
    LocalDateTime createdAt();
}

Object Creation

User user = UserDraft.$.produce(draft -> {
    draft.setName("Alice");
    draft.setEmail("[email protected]");
    draft.setCreatedAt(LocalDateTime.now());
});

Code Generation

Like Marten, Jimmer uses code generation at compile time:

  • Generates UserDraft for object creation
  • Generates UserTable for type-safe querying
  • Handles immutable object implementations

Development Environment Setup

Java/Jimmer Path (Working)

# Prerequisites
sudo apt install openjdk-17-jdk maven

# Project structure
mkdir marten-experiment && cd marten-experiment
# Create pom.xml with Jimmer dependencies
# Create entity interfaces
# Maven handles code generation automatically
mvn compile && mvn exec:java -Dexec.mainClass="com.example.JimmerExample"

.NET/Marten Path (Blocked)

Corporate laptop restrictions prevented .NET SDK installation. Options to explore:

  • Request IT approval for .NET SDK
  • Use WSL2 with different permissions
  • Try Mono (outdated, not recommended)
  • Use Visual Studio if available

Key Patterns Learned

1. Document vs Relational Storage

  • Relational: Normalize data across tables with foreign keys
  • Document: Store complete objects as JSON, query within documents
  • Hybrid: Marten/Jimmer allow both patterns in the same database

2. Code Generation Benefits

  • Define data shape once (class/interface)
  • Generated code handles:
    • Object creation/mutation
    • Serialization/deserialization
    • Type-safe query builders
    • Database schema management

3. Query Translation

  • Write queries in host language (C# LINQ, Java builder pattern)
  • Library translates to optimized SQL with JSON operators
  • Deferred execution - builds expression trees, executes when enumerated

Event Sourcing Context

Marten is popular for event sourcing because:

  • Events are naturally document-shaped (JSON)
  • PostgreSQL provides strong consistency guarantees
  • Built-in stream aggregation and projection capabilities
  • Can mix event data with traditional relational data

Next Steps

  1. Get .NET working - Either through IT approval or alternative approach
  2. Try real Marten examples - Document storage, event sourcing, projections
  3. Compare with current architecture - Understand migration implications
  4. Explore Marten's "warts" - Concurrency handling, performance characteristics, serialization edge cases

Working Code

The Jimmer experiment is ready to extend:

  • Location: ~/marten-hack/
  • Run: mvn exec:java -Dexec.mainClass="com.example.JimmerExample"
  • Generated code: target/generated-sources/annotations/com/example/UserTable.java
  • Add database operations once API is clarified

The PostgreSQL container is still running:

docker ps  # Check if marten-postgres is running
# Connect: docker exec -it marten-postgres psql -U postgres -d marten_test

OTHER STUFF

Your gist captures the essential concepts very well! You've got all the important parts. I'd suggest adding just a few missing pieces to make it complete:

1. Add the actual code files - You mention "Both java files go in src/main/java/com/example/" but don't show them. Include:

// User.java
package com.example;
import org.babyfish.jimmer.sql.*;
import java.util.UUID;
import java.time.LocalDateTime;

@Entity
public interface User {
    @Id
    UUID id();
    String name();
    String email();
    LocalDateTime createdAt();
}

2. Include the pom.xml since that was crucial for getting Jimmer working:

<!-- Key dependencies section -->
<dependency>
    <groupId>org.babyfish.jimmer</groupId>
    <artifactId>jimmer-sql</artifactId>
    <version>0.8.138</version>
</dependency>

3. Add one important insight you discovered - the "SQL backwards" LINQ concept:

LINQ is "SQL backwards":
- SQL: SELECT name FROM users WHERE age > 25 ORDER BY name
- LINQ: users.Where(u => u.Age > 25).OrderBy(u => u.Name).Select(u => u.Name)

4. Mention the Docker PostgreSQL setup since that was essential:

docker run --name marten-postgres \
  -e POSTGRES_PASSWORD=password \
  -e POSTGRES_DB=marten_test \
  -p 5432:5432 -d postgres:15

Everything else is spot on! You've captured the key insights about document storage, code generation, and the conceptual similarities between Marten and Jimmer.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>marten-hack</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Core Jimmer -->
<dependency>
<groupId>org.babyfish.jimmer</groupId>
<artifactId>jimmer-sql</artifactId>
<version>0.8.138</version>
</dependency>
<!-- PostgreSQL driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<!-- JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Annotation processing for Jimmer -->
<dependency>
<groupId>org.babyfish.jimmer</groupId>
<artifactId>jimmer-apt</artifactId>
<version>0.8.138</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.babyfish.jimmer</groupId>
<artifactId>jimmer-apt</artifactId>
<version>0.8.138</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.example;
import org.babyfish.jimmer.sql.JSqlClient;
import org.babyfish.jimmer.sql.runtime.ConnectionManager;
import java.sql.Connection;
import java.sql.DriverManager;
import java.time.LocalDateTime;
import java.util.List;
public class JimmerExample {
public static void main(String[] args) throws Exception {
// Simple connection manager with proper functional interface
ConnectionManager connectionManager = new ConnectionManager() {
@Override
public <R> R execute(java.util.function.Function<Connection, R> block) {
try (Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost:5432/marten_test",
"postgres",
"password")) {
return block.apply(conn);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
// Create JSqlClient
JSqlClient sqlClient = JSqlClient.newBuilder()
.setConnectionManager(connectionManager)
.build();
System.out.println("Jimmer setup complete - like Marten but for Java!");
System.out.println("UserTable class was generated at: target/generated-sources/annotations/com/example/UserTable.java");
// Let's just demonstrate that Jimmer is working by creating objects
// We'll skip the database operations for now since the API is tricky
// Create a user using Jimmer's generated draft (like Marten's object creation)
User user = UserDraft.$.produce(draft -> {
draft.setName("Alice");
draft.setEmail("[email protected]");
draft.setCreatedAt(LocalDateTime.now());
});
System.out.println("Created user with Jimmer:");
System.out.println("- Name: " + user.name());
System.out.println("- Email: " + user.email());
System.out.println("- Created: " + user.createdAt());
// This shows the key Marten-like concept: type-safe object creation
// The actual database operations would be similar to what we did with raw JSONB
System.out.println("\nThis demonstrates the core Marten pattern:");
System.out.println("1. Define entity interfaces");
System.out.println("2. Generated code handles the implementation");
System.out.println("3. Type-safe object creation and querying");
}
}
package com.example;
import org.babyfish.jimmer.sql.*;
import java.util.UUID;
import java.time.LocalDateTime;
@Entity
public interface User {
@Id
UUID id();
String name();
String email();
LocalDateTime createdAt();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment