uptrace / bun

SQL-first Golang ORM
https://bun.uptrace.dev
BSD 2-Clause "Simplified" License
3.8k stars 231 forks source link

Configuration-based database connection in 1 line #579

Closed kushuh closed 1 year ago

kushuh commented 2 years ago

The issue

I love bun for its simplicity, but initialization is a pain point. Basically, I have many repos where I always need to do only 2 things:

I find the steps to do so quite verbose (in a bad way) and have written a dedicated utils package just for this.

Currently, I have to do something along those lines (to start the connection and run migrations):

var (
    dsn string
    migrationsGroups []fs.FS
)

// Here I get my DSN and migration files, not relevant for what I want to propose.

driver := pgdriver.WithDSN(dsn)
psql := sql.OpenDB(pgdriver.NewConnector(driver))
database := bun.NewDB(psql, pgdialect.New())

migrations := migrate.NewMigrations()
for i, migrationGroup := range migrationsGroups {
    if err := migrations.Discover(migrationGroup); err != nil {
        panic(err)
    }
}

migrator := migrate.NewMigrator(database, migrations)
if err := migrator.Init(context.Background()); err != nil {
    return nil, err
}

if _, err := migrator.Migrate(context.Background()); err != nil {
    panic(err)
}

This is a lot of lines for a very simple need (and quite tedious to figure out for someone new to the ORM).

The proposal

This ORM does it in a way I find more elegant: https://github.com/go-redis/redis (I can see you also contribute to it @vmihailenco 😁).

The idea would be, for cases that require minimal configuration, to allow a 1-line method that would ressemble this, and would handle all the "dirty" work:

database := bun.NewClient(options)

Where options would be a single configuration object with optional values.

I have some ideas for how to handle things such as switching dialects and running migrations (I already done some work in my repositories with Postgres driver). With some approval I can start working on a more concrete proposition (PR? Design Document?) as soon as this weekend.

Pros

=> Mostly sugar-syntax, so testing should be fast and easy => Does not break anything, only addition to current apis => More versatile, easier to read, less verbose for simple configurations => Better errors handling (a single failure point instead of many)

Example

That's something ressembling the solution I'd come up with right now:

database := bun.NewClient(bun.ClientConfig{
    Driver: bun.PostgresDriver,
    DSN: dsn,
    Migrations: bun.FileMigrations{migrationsGroups...},
})

Or:

database := bun.NewClient(bun.PostgresConfig{
    DSN: dsn,
    Migrations: bun.FileMigrations{migrationsGroups...},
})
vmihailenco commented 2 years ago

It is impossible to have bun.PostgresConfig without adding a dependency on pgdriver and pgdialect, which makes MySQL users unahppy and does not allow to use pgx.

Perhaps you should just extract the functionality you need to a package in your repo.

kushuh commented 2 years ago

I wonder if it could be possible to have a config or params module, that would work similarly to driver and dialect (with specialized sub-modules to separate dependencies).

config
 \_ sqlconfig
 \_ ...
 \_ pgconfig

The pgconfig module could have 2 configuration objects, one for pgdriver and another for pgx.

It could also be a solution to completly not support pgx with configuration based instantiation, since it is more suited for simple use-cases and using pgx instead of the internal pgdriver would not fall in such a scenario (again it is a quick start solution that does not replace the current api, it doesn't have to cover every use-case).

This would still allow for a 1-line declaration with a configuration object, and would just involve to do the right imports (kind of like it already works for drivers and dialects actually).

I can get away with sticking to my own repositories but it is still frustrating to not have a generic solution πŸ˜…. Driver-specific configuration could still be passed by importing the right module without conflict.

Plus migrations seem to work equally no matter which driver is used, and there is only 2 of them (go-based and sql-based), which I think can be handled pretty easily in a configuration object (especially now that go has generics). I tend to believe configuration patterns are more readable than declarative one, easier to understand, and especially with go tedious error handling, having a single failure point is another argument for me.

But maybe all of this is too much opinion-based. I'd still be happy to have an integrated solution, but for now I'll work with my own package πŸ™‚.

kushuh commented 2 years ago

To complete my last message, it could ressemble something like this:

db, err := config.NewClient(config.Config{
    Driver: pgconfig.Config{ // This could possibly be handled elegantly with generics?
        DSN: "blablabla",
    },
    // Not scoped since not specific to a driver, maybe there is a more elegant solution.
    Migrations: &config.Migrations{ 
        Files: []fs.FS{/* list of FS to apply Discover to */},
        // IDK what type to put here, I don't use Go migrations that much.
        Go: []interface{}, 
    }
})

Or another use-case:

db, err := config.NewClient(config.Config{
    Driver: pgconfig.PGXConfig{DSN: "blablabla"},
})

EDIT

Each config object could ship with class methods that would return a *bun.DB instance, so that the main package only calls this method (eg: Init or Connect). Thus the Driver key could be of type interface, where the interface only contains one method to create the *bun.DB object.

type DriverConfig interface{
    // No circular deps because `config` is never called from the main package.
    Connect() (*bun.DB, error) 
}

type Config struct {
    Driver DriverConfig
    Migrations // ...
}

I also have doubt about the name config, but basically the idea would be to have a "quickstart" module with preconfigured 1-line handlers.

kushuh commented 2 years ago

If someone is interested in the issue or wants an alternative solution, I made a "poc" of my idea based on previous work. I'll use it for now, not loosing hope on having a native solution one day πŸ™‚.

https://gitlab.com/a-novel/go-tools/bun-configurator