Packages

A functional, monadic wrapper for Mnesia built with category theory principles. Features: auto-increment counters, TTL with automatic cleanup, backup/restore, real-time events, and a clean CRUD API. Pure functional programming throughout.

Current section

Files

Jump to
mnesia_ex examples 07_transactions.exs
Raw

examples/07_transactions.exs

# MnesiaEx.transaction - Composable Transactions
#
# This example demonstrates using MnesiaEx.transaction/1
# similar to Ecto's Repo.transaction/1 for composable operations.
#
# Run with: elixir examples/07_transactions.exs
Mix.install([
{:mnesia_ex, path: "."}
])
defmodule MyApp.Users do
use MnesiaEx, table: :users
end
defmodule MyApp.Posts do
use MnesiaEx, table: :posts
end
IO.puts("Starting MnesiaEx...")
MnesiaEx.start()
IO.puts("Creating schema...")
{:ok, :created} = MnesiaEx.Schema.create([node()])
# Create tables
IO.puts("Creating tables...")
MyApp.Users.create(
attributes: [:id, :name, :email],
counter_fields: [:id],
type: :set,
persistence: false
)
MyApp.Posts.create(
attributes: [:id, :user_id, :title, :content],
counter_fields: [:id],
type: :set,
persistence: false
)
IO.puts("\n=== Smart Auto-Transaction Detection ===\n")
# Functions work standalone (auto-transaction)
IO.puts("Writing user standalone (auto-transaction)...")
{:ok, standalone_user} = MyApp.Users.write(%{id: 1, name: "Standalone", email: "standalone@example.com"})
IO.inspect(standalone_user, label: "✅ Created with auto-transaction")
# Same function inside transaction (detected, no double-wrap)
IO.puts("\n=== Composing Operations in Manual Transaction ===\n")
IO.puts("Creating user and post in one transaction...")
{:ok, {user, post}} = MnesiaEx.transaction(fn ->
{:ok, user_id} = MyApp.Users.get_next_id(:id)
{:ok, user} = MyApp.Users.write(%{id: user_id, name: "Alice", email: "alice@example.com"})
# ↑ Detects existing transaction, doesn't create another
{:ok, post_id} = MyApp.Posts.get_next_id(:id)
{:ok, post} = MyApp.Posts.write(%{id: post_id, user_id: user.id, title: "My First Post", content: "Hello!"})
# ↑ Detects existing transaction, doesn't create another
{user, post}
end)
IO.inspect(user, label: "Created user")
IO.inspect(post, label: "Created post")
IO.puts("💡 Both operations in ONE transaction (auto-detected)")
IO.puts("\n=== Transaction with Error Handling ===\n")
# Transaction that might fail
IO.puts("Attempting to create post for non-existent user...")
result = MnesiaEx.transaction(fn ->
{:ok, post_id} = MyApp.Posts.get_next_id(:id)
# This will fail because user 999 doesn't exist
case MyApp.Users.read(999) do
{:ok, user} ->
MyApp.Posts.write(%{id: post_id, user_id: user.id, title: "Post", content: "..."})
{:error, :not_found} ->
:mnesia.abort(:user_not_found)
end
end)
case result do
{:ok, post} ->
IO.puts("Post created: #{inspect(post)}")
{:error, :user_not_found} ->
IO.puts("Transaction aborted: User not found")
end
IO.puts("\n=== Composing Multiple Operations ===\n")
# Create a user with multiple posts
IO.puts("Creating user with 3 posts...")
{:ok, {user, posts}} = MnesiaEx.transaction(fn ->
{:ok, user_id} = MyApp.Users.get_next_id(:id)
{:ok, user} = MyApp.Users.write(%{id: user_id, name: "Bob", email: "bob@example.com"})
post_data = [
%{user_id: user.id, title: "Post 1", content: "First post"},
%{user_id: user.id, title: "Post 2", content: "Second post"},
%{user_id: user.id, title: "Post 3", content: "Third post"}
]
{:ok, posts} = Enum.reduce_while(post_data, {:ok, []}, fn data, {:ok, acc} ->
{:ok, post_id} = MyApp.Posts.get_next_id(:id)
case MyApp.Posts.write(Map.put(data, :id, post_id)) do
{:ok, post} -> {:cont, {:ok, [post | acc]}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
{user, Enum.reverse(posts)}
end)
IO.inspect(user, label: "User")
IO.puts("Created #{length(posts)} posts for user #{user.id}")
IO.puts("\n=== Atomic Update of Multiple Records ===\n")
# Update multiple records atomically
IO.puts("Updating all Bob's posts to mark as published...")
{:ok, updated_posts} = MnesiaEx.transaction(fn ->
bob_posts = MyApp.Posts.select([{:user_id, :==, user.id}]) # select returns list directly
Enum.map(bob_posts, fn post ->
{:ok, updated} = MyApp.Posts.update(post.id, %{title: "[Published] #{post.title}"})
updated
end)
end)
IO.puts("Updated #{length(updated_posts)} posts")
IO.inspect(List.first(updated_posts), label: "First updated post")
IO.puts("\n=== Transaction Rollback on Error ===\n")
# This transaction will fail and rollback all changes
IO.puts("Attempting transaction that will fail...")
result = MnesiaEx.transaction(fn ->
{:ok, user_id} = MyApp.Users.get_next_id(:id)
{:ok, _user} = MyApp.Users.write(%{id: user_id, name: "Temp", email: "temp@example.com"})
{:ok, post_id} = MyApp.Posts.get_next_id(:id)
{:ok, _post} = MyApp.Posts.write(%{id: post_id, user_id: user_id, title: "Temp Post", content: "..."})
# Abort the transaction on purpose
:mnesia.abort(:intentional_failure)
end)
case result do
{:ok, _} ->
IO.puts("Transaction succeeded (unexpected)")
{:error, :intentional_failure} ->
IO.puts("Transaction rolled back as expected!")
# Verify the temp user and post were NOT created
temp_users = MyApp.Users.select([{:name, :==, "Temp"}]) # select returns list directly
temp_users
|> length()
|> then(fn
0 -> IO.puts("✅ User was not created (rollback worked)")
_ -> IO.puts("❌ User was created (rollback failed)")
end)
end
IO.puts("\n=== Comparison: ! vs transaction ===\n")
IO.puts("Using ! functions (automatic transaction):")
user_with_bang = MyApp.Users.write!(%{id: 100, name: "BangUser", email: "bang@example.com"})
IO.inspect(user_with_bang, label: "User (with !)")
IO.puts("\nUsing transaction manually (composable):")
{:ok, user_manual} = MnesiaEx.transaction(fn ->
{:ok, user_id} = MyApp.Users.get_next_id(:id)
MyApp.Users.write(%{id: user_id, name: "ManualUser", email: "manual@example.com"})
end)
IO.inspect(user_manual, label: "User (manual transaction)")
IO.puts("\n=== API Comparison ===\n")
IO.puts("1️⃣ Bang functions (!) - Convenience for single operations:")
IO.puts(" user = MyApp.Users.write!(%{...})")
IO.puts(" ✅ Auto-transaction + returns value")
IO.puts(" ✅ Raises on error")
IO.puts("")
IO.puts("2️⃣ Non-bang standalone - Auto-transaction with error handling:")
IO.puts(" {:ok, user} = MyApp.Users.write(%{...})")
IO.puts(" ✅ Auto-transaction (detected NOT in transaction)")
IO.puts(" ✅ Returns {:ok, value} | {:error, reason}")
IO.puts("")
IO.puts("3️⃣ Non-bang inside MnesiaEx.transaction - Composable:")
IO.puts(" MnesiaEx.transaction(fn ->")
IO.puts(" {:ok, user} = MyApp.Users.write(%{...})")
IO.puts(" {:ok, post} = MyApp.Posts.write(%{...})")
IO.puts(" end)")
IO.puts(" ✅ Detects existing transaction (NO double-wrap)")
IO.puts(" ✅ All operations atomic")
IO.puts("")
IO.puts("4️⃣ dirty_* functions - Speed over consistency:")
IO.puts(" {:ok, cache} = MyApp.Cache.dirty_write(%{...})")
IO.puts(" ✅ No transaction (10x faster)")
IO.puts(" ❌ No ACID guarantees")
IO.puts("")
IO.puts("💡 When to use each:")
IO.puts("- ! functions: Quick operations, prototyping")
IO.puts("- Non-bang standalone: Error handling, pattern matching")
IO.puts("- MnesiaEx.transaction: Composing multiple operations atomically")
IO.puts("- dirty_*: High-performance caching, non-critical data")
# Cleanup
IO.puts("\n=== Cleanup ===\n")
MyApp.Users.drop()
MyApp.Posts.drop()
{:ok, :deleted} = MnesiaEx.Schema.delete([node()])
MnesiaEx.stop()
IO.puts("Done!")