Search code examples
databasegomulti-tenant

How to make database connection switchable at runtime when using SQLC


I know this question is asked already many times, but I did not find a good answer for my case. I'm using SQLC to generate methods for querying the db. Everything is working fine when using one connection initialised at start. Now I need to set it up in a multi-tenant environment where each tenant will have a separate DB. For now I would like to start with a connection map (map[string]*sql.DB) connecting the tenant with a database connection. My question is about overriding/selecting the connection at runtime. with one connection the repository is initialised like:

type Repository interface {
    GetCustomerById(ctx context.Context, id int64) (Customer, error)
    ListCustomers(ctx context.Context) ([]Customer, error)
}

type repoSvc struct {
    *Queries
    db *sql.DB
}

func NewRepository(dbconn *sql.DB) Repository {
    return &repoSvc{
        Queries: New(dbconn),
        db:      dbconn,
    }
}

customerRepo := customerRepo.NewRepository(conn)

GetCustomerById is the SQLC generated method conn is the database connection

How to make the connection based on a parameter (from cookie or context)?


Solution

  • The simplest way, assuming you are using separate databases, is to maintain a map[tenantID]Repository, where tenantID is the way you differentiate between tenants (e.g. a string or uint that contains the tenant ID).

    This way you can do everything at runtime:

    • when you need to add a tenant, just instantiate the Repository for that tenant and add it to the map
    • when you need to remove a tenant, just remove its Repository from the map and close the DB connection
    • when you need to perform a query for a tenant, lookup the corresponding Repository in the map, and use it to perform the query for that tenant

    If the operations above may happen concurrently, make sure that you're using some synchronization mechanism to avoid data races when accessing the map (e.g. sync.Map, or sync.RWMutex).

    If you have a database table that stores the tenants and their DB connection URIs, you can still use this approach: when you need to perform a query check if the Repository exists in the map: if it's missing, query the tenant table and add the Repository for that tenant to the map. You can then periodically scan the map and remove any Repository that has not been used for some time.

    To make all of this easier you could also wrap the whole machinery into a MultitenantRepository interface, that is identical to the Repository interface but that accepts an additional tenantID parameter on each method:

    type MultitenantRepository interface {
        GetCustomerById(ctx context.Context, tenant tenantID, id int64) (Customer, error)
        ListCustomers(ctx context.Context, tenant tenantID) ([]Customer, error)
    }
    

    This will avoid exposing all the complexity of your multitenant setup to your business logic.