Hey @svoop, thanks for sharing this!
I’ve taken a look at your demo app, and it did help me discover one bug: Hanami::DB::Testing.database_url should apply to explicitly configured database URLs, not just ENV-detected URLs · Issue #1482 · hanami/hanami · GitHub. It was this bug that was causing this warning to print when running commands like hanami db migrate
:
WARNING: Database svoop_demo_test expects migrations to be located within slices/admin/config/db/migrate/ but that folder does not exist.
The workaround for now here is to remove/comment out the explicit datase_url
assignment line from your db provider:
Hanami.app.configure_provider :db do
config.gateway :default do |gw|
# gw.database_url = "postgres://postgres@localhost/svoop_demo"
end
end
With this change, Hanami will then pick up the database URL from the DATABASE_URL
ENV var.
Two things related things happen that lead to that warning above:
- When a
hanami db
command is run in the development environment, it also re-runs itself in the test environment, to make sure the test databases are kept in sync.
- Our default behaviour is to append a
_test
string to a database URL when running in the test environment. We currently do this for ENV-based database URLs, but not for explicitly configured ones (hence the issue I linked above). So when the hanami db
command executed for your app, it picked up a database URL from both the ENV (since you have it in your .env
file as well as your explicitly configured URL. This led Hanami to mistakenly believe you had two databases configured instead of one, because they each had different URLs (one with _test
appended and one not).
So by removing the explicitly configured URL for now, the warning no longer prints, because Hanami resolves just a single database URL for your app, and it knows that database is already configured via the config/db/
folder you have:
$ bundle exec hanami db migrate
NOTE: Empty database migrations folder (config/db/migrate/) for svoop_demo
=> svoop_demo structure dumped to config/db/structure.sql in 0.0382s
NOTE: Empty database migrations folder (config/db/migrate/) for svoop_demo_test
But none of this addresses your concerns about config.db.import_from_parent
and its default value.
It’s important to clarify something here. There are actually two related settings:
config.db.configure_from_parent # default is true
config.db.import_from_parent # default is false
configure_from_parent
will copy DB config from parent slices to child slices, but allow the child slice to have its own fully independent DB setup thereafter. So the slice will have its own relations, etc.
import_from_parent
, on the other hand, will take the entire DB setup from the parent slice and simply register it into the child slice. You would use this setting if you only want a single set of relations for your entire app (e.g. defined in app/relations/
), and then have those relations made available in every slice.
Your demo app is fairly minimal, so it’s not clear which of these you actually want.
But here’s how you can verify the two different modes:
config.db.configure_from_parent = true
(our default)
To verify this, add some kind of config to the parent slice’s DB provider, for example:
# config/providers/db.rb
Hanami.app.configure_provider :db do
config.gateway :default do |gw|
config.adapter :sql do |a|
a.extension :exclude_or_null
end
end
end
Then verify that the config is also set in a child slice. So in your demo app’s case, the admin slice. Via the console:
# Note that :exclude_or_null is here
demo[development]> app["db.gateways.default"].options[:extensions]
=> [:exclude_or_null, :caller_logging, :error_sql, :sql_comments, :pg_array, :pg_enum, :pg_json, :pg_range]
# Same for the admin slice
demo[development]> admin["db.gateways.default"].options[:extensions]
=> [:exclude_or_null, :caller_logging, :error_sql, :sql_comments, :pg_array, :pg_enum, :pg_json, :pg_range]
The configure_from_parent
mode makes sure that all possible configs are taken from the parent slice and also configured on the DB setup created in child slices.
In this mode, the config is copied, but the actual ROM containers are distinct to each slice:
demo[development]> app["db.rom"].object_id
=> 84940
demo[development]> admin["db.rom"].object_id
=> 91040
This is what allows you to create your unique relations to each slice. For example, if I updated that create_posts.rb
migration to actually create a table, and then ran hanami g relation posts
and hanami g relation posts --slice=admin
, you’ll see that each slice’s ROM container only sees its own version of the posts relation:
demo[development]> app["db.rom"].relations[:posts]
=> #<Demo::Relations::Posts name=ROM::Relation::Name(posts) dataset=#<Sequel::Postgres::Dataset: "SELECT \"posts\".\"title\" FROM \"posts\"">>
demo[development]> admin["db.rom"].relations[:posts]
=> #<Admin::Relations::Posts name=ROM::Relation::Name(posts) dataset=#<Sequel::Postgres::Dataset: "SELECT \"posts\".\"title\" FROM \"posts\"">>
So in this arrangement:
- DB config is shared. This so that you only have to set that config in one place, rather than copy it into every slice, which is an important convenience).
- But everything else afterwards is specific to each slice.
config.db.import_from_parent = true
(not the default)
Now if I add this line to either config/app.rb
or your config/slices/admin.rb
(either place is fine, since slices will inherit their config from their parent), we see some different behaviour. The ROM containers for both the app and slices are identical (the literal same object), and as such, only a single shared set of relations is available in all places.
demo[development]> app["db.rom"].object_id
=> 17380
demo[development]> admin["db.rom"].object_id
=> 17380
demo[development]> app["db.rom"].relations[:posts]
=> #<Demo::Relations::Posts name=ROM::Relation::Name(posts) dataset=#<Sequel::Postgres::Dataset: "SELECT \"posts\".\"title\" FROM \"posts\"">>
demo[development]> admin["db.rom"].relations[:posts]
=> #<Demo::Relations::Posts name=ROM::Relation::Name(posts) dataset=#<Sequel::Postgres::Dataset: "SELECT \"posts\".\"title\" FROM \"posts\"">>
So in this arrangement:
- DB config is centralised, by virtue of a single ROM setup being used everywhere.
- Relations are shared across all slices. This is convenient if your app is small and you don’t want the hassle of defining and managing a distinct set of relations for each slice.
- Slices still have their own repos and structs on top of those shared relations, since repos and structs those aren’t managed via the ROM container.
All of the examples above come from me working directly on your demo app, so I’m quite confident everything is working as it should. We also have tests covering these cases.
I think we could do a lot more to document these different modes. Our DB docs are still a work in progress, and we’ll add more detail when we can.
I’m really grateful you raised this though, because it’s made it clearer the need to document these better, as well as helping me find that bug! Thank you!
If you have any more questions, or if you think I’ve missed something from your original report, please let me know