commanded / eventstore

Event store using PostgreSQL for persistence
MIT License
1.04k stars 142 forks source link

How to create/init a dynamic EventStore? #264

Closed suzdalnitski closed 1 year ago

suzdalnitski commented 1 year ago

Hi,

I'm trying to start multiple Commanded applications and EventStores dynamically (one EventStore per Commanded Application). As far as I understand, Commanded will start the event store automatically whenever an application is started. However once I try dispatching commands, I get an error: relation "event_store_default.streams\ does not exist. Which makes sense, because I haven't created/initialized the event stores using the other schemas.

How do I configure the event_stores to use a different schema per event store? I've tried:

config :my_app,
  event_stores: [
    {MyApp.Commanded.EventStore, schema: "eventstore_default"},
    {MyApp.Commanded.EventStore, schema: "eventstore_other"},
  ]

And then ran event_store.create, which didn't work, because it expects modules, and I'm not sure where to put the schema config option.

My Commanded application:

defmodule MyApp.Commanded.Application do
  use Commanded.Application, otp_app: :my_app
  def init(config) do
    event_store_name = Keyword.fetch!(config, :event_store_name)
    event_store_schema = Keyword.fetch!(config, :event_store_schema)

    config = put_in(config, [:event_store, :name], event_store_name)

    # Not sure if this should be `prefix` or `schema`? The docs use both in different places.
    config = put_in(config, [:event_store, :schema], event_store_schema)

    {:ok, config}
  end
end

I've been following the guide here, but it didn't mention anything about creating/initializing the event stores: https://github.com/commanded/commanded-eventstore-adapter/blob/master/guides/Dynamic%20Event%20Store.md

Thanks!

slashdotdash commented 1 year ago

For dynamic event stores you cannot use the Mix tasks provided by the event store library. Instead you will need to create your own Mix task - or release tasks module for deployment - and manually call the event store tasks to create, init, and migrate the separate event store schemas.

You need to include the dynamic schema in the event store's config:

alias EventStore.Tasks.{Create, Init, Migrate}
alias MyApp.Commanded.EventStore

for schema <- ["eventstore_default", "eventstore_other"] do
  config = EventStore.config() |> Keyword.merge(pool_size: 1, schema: schema)

  Create.exec(config, quiet: true)
  Init.exec(config, quiet: true)
  Migrate.exec(config, quiet: true)
end

For deployment using a release tasks module see the Phoenix deploying with releases documentation where they have an example of Ecto migrations and custom commands. Example show below.

defmodule MyApp.Release do
  alias EventStore.Tasks.{Create, Init, Migrate}
  alias MyApp.Commanded.EventStore

  @app :my_app

  def migrate do
    load_app()

    for schema <- ["eventstore_default", "eventstore_other"] do
      config = EventStore.config() |> Keyword.merge(pool_size: 1, schema: schema)

      Create.exec(config, quiet: true)
      Init.exec(config, quiet: true)
      Migrate.exec(config, quiet: true)
    end
  end

  defp load_app do
    Application.load(@app)
  end
end

Then you'd run:

$ MIX_ENV=prod mix release
$ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"
suzdalnitski commented 1 year ago

Worked like a charm, thank you!

kingdomcoding commented 1 year ago

I think this information should be somewhere public, probably in the guides.

For anyone curious, here's what I modified my (autogenerated, as per the guide referred to above) releases module into for deployment using elixir releases

defmodule MyApp.Release do
  @moduledoc """
  Used for executing DB release tasks when run in production without Mix
  installed.
  """
  alias EventStore.Tasks.{Create, Init, Migrate}

  @app :my_app

  def migrate do
    load_app()

    for repo <- repos() do
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
    end
  end

  def rollback(repo, version) do
    load_app()
    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
  end

  def setup_event_stores do
    load_app()

    for event_store <- event_stores() do
      config = event_store.config()

      Create.exec(config, quiet: true)
      Init.exec(config, quiet: true)
      Migrate.exec(config, quiet: true)
    end
  end

  defp repos do
    Application.fetch_env!(@app, :ecto_repos)
  end

  defp event_stores do
    Application.fetch_env!(@app, :event_stores)
  end

  defp load_app do
    Application.load(@app)
  end
end

setup_event_stores/0 is the interesting function

Then I ran

$ MIX_ENV=prod mix release
$ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"
$ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.setup_event_stores"