lucid icon indicating copy to clipboard operation
lucid copied to clipboard

How to dynamically adjust the model's namingStrategy during execution

Open NOCSF opened this issue 5 months ago • 9 comments

I have two databases, driven by MySQL and PostgreSQL respectively. The only difference between their libraries, tables, and fields is that one is all lowercase and the other is all uppercase. I have now implemented dynamic database connection switching, like this:

    // controller
    async debug({ response }: HttpContext) {
        await changeModelConnection('midea')
        return CmCluster.query().firstOrFail()
    }

export const changeModelConnection = async (clusterName: string) => {
    const config = (await Config.query().where('clusterName', clusterName).firstOrFail()).self

    if (db.manager.has('cm')) {
        await db.manager.release('cm')
    }

    @ts-ignore
TODO--Remember to remove ↑ after the types are improved
strCase = FKDB.dbStringFormat[FKDB.dbAlias[config.db.client]]

console.log(`changeModelConnection.strCase:${strCase}`)
}

I hope the model can implement a dynamic namingStrategy to properly handle the fields in the database, can this be done?

NOCSF avatar Jul 25 '25 08:07 NOCSF

you already had a look into the docs? https://lucid.adonisjs.com/docs/model-naming-strategy#naming-strategy

thedomeffm avatar Jul 25 '25 08:07 thedomeffm

I've defined a #naming-strategy, but I can't switch the namingStrategy used by the model during runtime

Here's my namingStrategy:

import { CamelCaseNamingStrategy } from '@adonisjs/lucid/orm'
import type { LucidModel } from '@adonisjs/lucid/types/model'
import string from '@adonisjs/core/helpers/string'
import { ModelRelations } from '@adonisjs/lucid/types/relations'

export class DynamicNameStrategy extends CamelCaseNamingStrategy {
    private case: keyof Pick<String, 'toUpperCase' | 'toLowerCase'>

    constructor(caseType: keyof Pick<String, 'toUpperCase' | 'toLowerCase'>) {
        super()
        this.case = caseType
    }

    // 表名处理(如果 static table 已定义,直接转换;否则从模型名生成)
    tableName(model: LucidModel): string {
        const tableName = super.tableName(model)
        return this.case === 'toUpperCase' ? tableName.toUpperCase() : tableName
    }

    // 列名处理(转为 SCREAMING_SNAKE_CASE 或保持原样)
    columnName(_: LucidModel, propertyName: string): string {
        const snakeCaseName = super.columnName(_, propertyName)
        return this.case === 'toUpperCase' ? snakeCaseName.toUpperCase() : snakeCaseName
    }

    // JSON 序列化字段名(与列名保持一致)
    serializedName(_: LucidModel, propertyName: string): string {
        return this.columnName(_, propertyName)
    }

    relationLocalKey(
        relation: ModelRelations<LucidModel, LucidModel>['__opaque_type'],
        model: LucidModel,
        relatedModel: LucidModel
    ): string {
        const key = super.relationLocalKey(relation, model, relatedModel)
        return this.case === 'toUpperCase' ? key.toUpperCase() : key
    }

    relationForeignKey(
        relation: ModelRelations<LucidModel, LucidModel>['__opaque_type'],
        model: LucidModel,
        relatedModel: LucidModel
    ): string {
        const key = super.relationForeignKey(relation, model, relatedModel)
        return this.case === 'toUpperCase' ? key.toUpperCase() : key
    }

    // 多对多中间表名(保持原样或转为大写)
    relationPivotTable(relation: 'manyToMany', model: LucidModel, relatedModel: LucidModel) {
        const tableName = super.relationPivotTable(relation, model, relatedModel)
        return this.case === 'toUpperCase' ? tableName.toUpperCase() : tableName
    }

    // 中间表外键(保持原样或转为大写)
    relationPivotForeignKey(_relation: 'manyToMany', model: LucidModel) {
        const key = super.relationPivotForeignKey('manyToMany', model)
        return this.case === 'toUpperCase' ? key.toUpperCase() : key
    }

    paginationMetaKeys() {
        return super.paginationMetaKeys()
    }
}

NOCSF avatar Jul 25 '25 10:07 NOCSF

Wouldn't it be easier to simply have two models?

RomainLanz avatar Jul 25 '25 10:07 RomainLanz

That's the case, if I define different models, I need double the workload. But they are just different naming strategies between them, the association relationships are all the same. I have about 5 such clusters, each cluster has the same database tables. The difference between them is just the naming strategy.

To put it another way, I'm not sure if I'll continue to expand them in the future, so I hope to keep their associations, field definitions, and just change the naming strategy and database connection address

NOCSF avatar Jul 25 '25 13:07 NOCSF

You can have two models but keep fields and relations definitions in one place. That's the power of OOP and inheritance.

class BaseWhatever {
  @column()
  ....
}

class MySQLWhatever extends BaseWhatever {
  static namingStrategy = new MyCustomNamingStrategy()
}

class PSQLWhatever extends BaseWhatever {
  static namingStrategy = new MyCustomNamingStrategy()
}

RomainLanz avatar Jul 25 '25 13:07 RomainLanz

Then dynamically import based on certain conditions? I'm also thinking along these lines, but I feel it's not as convenient as changing namingStrategy, because I still have to define an object extra

Image

NOCSF avatar Jul 25 '25 13:07 NOCSF

And I mainly wanted to verify if my approach is feasible, it seems like you prefer the OOP way more

NOCSF avatar Jul 25 '25 13:07 NOCSF

Since, you have access to the Model constructor with all methods of a naming strategy. Maybe you can check for the connection name from the model and then decide how to format the column names?

thetutlage avatar Jul 25 '25 16:07 thetutlage

Unfortunately, I tried the inheritance approach. I found it doesn't work: even when I defined and used namingStrategy, the model still seems to follow the parent model's declare, unless I redefine the fields in the child model.

Here's the model definition:

// parent
import { BaseModel, column, hasMany } from '@adonisjs/lucid/orm'
import CmHost from './cm_host.js'
import { HasMany } from '@adonisjs/lucid/types/relations'
import CmService from './cm_service.js'

export default class CmCluster extends BaseModel {
    static connection = 'cm'

    @column({ isPrimary: true })
    declare clusterId: number

    @column()
    declare name: string

    @hasMany(() => CmHost, { foreignKey: 'clusterId' })
    declare hosts: HasMany<typeof CmHost>

    @hasMany(() => CmService, { foreignKey: 'clusterId' })
    declare services: HasMany<typeof CmService>
}
// upper
import { DynamicNameStrategy } from '#core/lucid/DynamicNameStrategy'
import { column, hasMany } from '@adonisjs/lucid/orm';
import CmCluster from './cm_cluster.js'
import CmHost from './cm_host.js';
import { HasMany } from '@adonisjs/lucid/types/relations';
import CmService from './cm_service.js';

export default class CmClusterUpper extends CmCluster {
    static table = 'CLUSTERS'
    static namingStrategy = new DynamicNameStrategy('toUpperCase')
}
// lower
import { BaseModel, column, hasMany } from '@adonisjs/lucid/orm'
import CmHost from './cm_host.js'
import { HasMany } from '@adonisjs/lucid/types/relations'
import CmService from './cm_service.js'
import { DynamicNameStrategy } from '#core/lucid/DynamicNameStrategy';
import CmCluster from './cm_cluster.js';

export default class CmClusterLower extends CmCluster {
    static table = 'clusters'
    static namingStrategy = new DynamicNameStrategy('toLowerCase')
}
// controller & console
const newConnectionModel = changeModelConnection({
    client: 'mysql2',
    connection: {
        host: '127.0.0.1',
        port: 3306,
        user: 'root',
        password: '***',
        database: 'cm_mysql',
    },
})('midea')
const newModel = await newConnectionModel(CmCluster)

console.log(await (newModel as unknown as typeof CmCluster).query())

return await (newModel as unknown as typeof CmCluster).query()

//-------console-------
[
  CmClusterUpper {
    modelOptions: { connection: 'cm' },
    modelTrx: undefined,
    transactionListener: [Function: bound listener],
    fillInvoked: false,
    cachedGetters: {},
    forceUpdate: false,
    '$columns': {},
    '$attributes': {},
    '$original': {},
    '$preloaded': {},
    '$extras': {
      CLUSTER_ID: 1,
      NAME: '***',
      MAINTENANCE_COUNT: 0,
      OPTIMISTIC_LOCK_VERSION: 770,
      CDH_VERSION: '***',
      DISPLAY_NAME: '***',
      UUID: '***',
      DATA_CONTEXT_ID: null,
      CLUSTER_TYPE: 'BASE_CLUSTER',
      EXCLUSIVE_LOCK_VERSION: 625
    },
    '$sideloaded': {},
    '$isPersisted': true,
    '$isDeleted': false,
    '$isLocal': false
  }
]

Obviously, all the queried fields are in the $extras section

This definition will work:

// upper full
import { DynamicNameStrategy } from '#core/lucid/DynamicNameStrategy'
import { column, hasMany } from '@adonisjs/lucid/orm'
import CmCluster from './cm_cluster.js'
import CmHost from './cm_host.js'
import { HasMany } from '@adonisjs/lucid/types/relations'
import CmService from './cm_service.js'

export default class CmClusterUpper extends CmCluster {
    static table = 'CLUSTERS'
    static namingStrategy = new DynamicNameStrategy('toUpperCase')

    @column({ isPrimary: true })
    declare clusterId: number

    @column()
    declare name: string

    @hasMany(() => CmHost, { foreignKey: 'clusterId' })
    declare hosts: HasMany<typeof CmHost>

    @hasMany(() => CmService, { foreignKey: 'clusterId' })
    declare services: HasMany<typeof CmService>
}

NOCSF avatar Jul 28 '25 09:07 NOCSF