- NEVER cast parameters without explicit field listing:
cast(changeset, params, [:field1, :field2])- NOTcast(changeset, params, Map.keys(params)) - NEVER interpolate user input directly into queries - always use
^for parameter binding - ALWAYS use
redact: trueon password fields and sensitive data in schemas - ALWAYS validate and cast external input through changesets before database operations
- ALWAYS pair
unsafe_validate_uniquewithunique_constraint- validations alone cannot prevent race conditions - ALWAYS use database constraints (
unique_constraint,foreign_key_constraint,check_constraint) as the source of truth - NEVER implement "get or create" without proper
on_conflicthandling or constraint-based error recovery - ALWAYS use transactions (
Repo.transactionorEcto.Multi) for operations that must be atomic
- ALWAYS preload associations that will be accessed in loops to prevent N+1 queries
- PREFER
insert_all,update_all,delete_allfor bulk operations over iterating individual operations - ALWAYS add indexes for foreign keys:
create index(:table, [:foreign_key_id]) - USE
on_conflictfor upserts instead of separate get/insert operations
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id# USE embedded schemas for: tightly coupled data, value objects, form data
embeds_one :address, Address
embeds_many :line_items, LineItem
# SEPARATE embedded schema module:
defmodule Address do
use Ecto.Schema
embedded_schema do # Note: embedded_schema, not schema
field :street, :string
field :city, :string
end
enddef changeset(struct, params) do
struct
|> cast(params, [:field1, :field2]) # 1. ALWAYS list fields explicitly
|> validate_required([:field1]) # 2. Required fields
|> validate_length(:field1, min: 3) # 3. Format validations
|> validate_format(:email, ~r/@/) # 4. More format validations
|> unsafe_validate_unique(:email, Repo) # 5. DB validations (optional optimization)
|> unique_constraint(:email) # 6. CRITICAL: Always add constraint
|> foreign_key_constraint(:user_id) # 7. Referential integrity
endschema "posts" do
field :org_id, :integer # Tenant identifier
field :title, :string
belongs_to :user, User,
foreign_key: :user_id,
references: :id,
with: [org_id: :org_id] # Composite foreign key
end# TRANSFORM errors for API/UI consumption
def format_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end# SIMPLE transaction with auto-wrapping return values
Repo.transact(fn ->
user = Repo.insert!(user_changeset)
profile = Repo.insert!(%Profile{user_id: user.id})
{:ok, %{user: user, profile: profile}} # Wrapped in {:ok, _}
end)
# Returns: {:ok, %{user: user, profile: profile}} or {:error, reason}
# EXPLICIT rollback
Repo.transact(fn ->
user = Repo.insert!(user_changeset)
if invalid_condition?(user) do
Repo.rollback(:invalid_user) # Returns {:error, :invalid_user}
end
{:ok, user}
end)
# REPO parameter variant (useful for testing/dependency injection)
Repo.transact(fn repo ->
user = repo.insert!(user_changeset)
{:ok, user}
end)- USE
Repo.transact/2for simple atomic operations with basic error handling - ALWAYS handle both success
{:ok, _}and failure{:error, _}cases - REMEMBER
transact/2auto-wraps successful returns, Multi requires explicit{:ok, result}
When generating Ecto code:
- SECURITY FIRST: Never compromise on parameter casting, always use changesets
- DATA INTEGRITY SECOND: Always use database constraints, handle race conditions
- PERFORMANCE THIRD: Prevent N+1 queries, use bulk operations when possible
- CLARITY FOURTH: Separate concerns, use multiple changesets, compose queries
- DO NOT generate
cast(params, Map.keys(params))- this is a critical security flaw - DO NOT forget
timestamps()in schemas - DO NOT use
validate_uniquewithoutunique_constraint - DO NOT query in loops without preloading
- DO NOT mix embedded schemas and associations incorrectly
- DO NOT forget to index foreign keys
- DO NOT use string interpolation in queries - always use
^for binding - DO NOT implement get-or-create without proper race condition handling
- DO NOT use a single changeset for all operations - separate by use case
- DO NOT ignore the return values of Repo operations - handle both success and error cases
- DO NOT create unnecessary indexes unless it will be used in a known query
- DO NOT change schemas and/or migrations to fix a test.
- DO NOT use varchar for column type instead of text
- DO NOT use String.to_atom to change an Ecto.enum into an atom before a cast