Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replacer interface for more flexible NamingStrategy #4042

Merged
merged 5 commits into from
Feb 14, 2021
Merged

Replacer interface for more flexible NamingStrategy #4042

merged 5 commits into from
Feb 14, 2021

Conversation

joelnordell
Copy link
Contributor

  • Do only one thing
  • Non breaking API changes
  • Tested

What did this pull request do?

This pull request makes NamingStrategy more flexible by allowing the user to provide a custom Replacer with an arbitrary implementation of Replace(). This change is fully backwards compatible: strings.NewReplacer continues to work as before.

Additionally, there is a new field named NoLowerCase which, when set true, will cause the NamingStrategy to skip converting names to snake_case. This is also backwards compatible: the default, when unspecified, is to keep the existing behavior. Definitely open to suggestions for naming that field if you prefer something different!

User Case Description

I am using GORM to interface with an existing sqlite database schema (Apple's Photos.app library) which uses naming conventions like ZGENERICASSET and ZFILENAME for its tables and fields. I would like to define my structs very simply like this, without requiring gorm tags to specify table or column names:

type GenericAsset struct { // No need to implement TableName(), this will become "ZGENERICASSET"
    FileName string        // No need for a tag, this will become "ZFILENAME"
}

With this change, I can achieve the above by using a simple NamingStrategy and no need to re-implement the full Namer interface:

type CustomReplacer struct{}
func (r CustomReplacer) Replace(name string) string {
    return "Z" + strings.ToUpper(name)
}
ns := schema.NamingStrategy{
    NameReplacer: CustomReplacer{},
    NoLowerCase: true,
}
...

@joelnordell joelnordell changed the title Replacer interface Replacer interface for more flexible NamingStrategy Feb 3, 2021
@jinzhu
Copy link
Member

jinzhu commented Feb 7, 2021

Hello @joelnordell

I think the globally smap should not exists or be part of the NamingStrategy struct, it will cause bug if your application using different NamingStrategy?

Could you make the change accordingly?

Thank you.

@joelnordell
Copy link
Contributor Author

Hi @jinzhu, yes I can make that change.

@joelnordell
Copy link
Contributor Author

joelnordell commented Feb 8, 2021

@jinzhu - I've implemented it. I ran into a couple of issues, though:

  • The NamingStrategy methods are all received by value, not by pointer.
  • The sync.Map cannot be copied by value, because it contains a Mutex.
  • The NamingStrategy is directly instantiated, and does not use a factory function.

Given the above constraints, and wanting to not change the public API at all, I had to do something a bit strange:

  • The smap is a pointer to a sync.Map which defaults to nil
  • All the places where smap is used first check for a nil pointer.
  • There is a new method Init() on NamingStrategy which is optional but will initialize the smap pointer.
  • gorm.Open detects whether schema.NamingStrategy was given as the Namer, and if so, calls Init() on it in order to initialize the smap.

It is a little bit ugly for gorm.Open to cast the Namer like that in order to call Init() - but I couldn't think of any other way to move smap into the struct without making a breaking change to the public API. (Ideas I considered and rejected were: changing all NamingStrategy method receivers to be pointers, or requiring a factory function to initialize NamingStrategy.)

Your thoughts?

This maintains backward compatibility by making the smap optional - the
NamingStrategy still works if it is nil. gorm.Open activates it by
calling Init() if the given Namer is a schema.NamingStrategy.

Also, this changes the key stored in the smap to be the original name,
instead of the replaced name.
@jinzhu
Copy link
Member

jinzhu commented Feb 9, 2021

Change NamingStrategy to pointer? then we can use sync.Map for smap

@joelnordell
Copy link
Contributor Author

Change NamingStrategy to pointer? then we can use sync.Map for smap

I thought about that, but it would be a breaking API change.

@jinzhu
Copy link
Member

jinzhu commented Feb 10, 2021

Change NamingStrategy to pointer? then we can use sync.Map for smap

I thought about that, but it would be a breaking API change.

Yes, sounds not good, but sounds like a better choice then currently...

Or maybe we should just remove the name caches, I don't think this helped the performance much if any. As we already cached the model's schema, which has the table/column name already, so seems not necessary to have a second level caches.

@joelnordell
Copy link
Contributor Author

joelnordell commented Feb 10, 2021

Removing the name cache sounds like a good plan. I'll make that change. I'll also check the benchmark tests to see if it actually does make a difference or not. (Update: running the benchmark tests before and after removing the name cache shows no difference.)

@joelnordell
Copy link
Contributor Author

@jinzhu - is this good to merge?

@jinzhu jinzhu merged commit 5744e29 into go-gorm:master Feb 14, 2021
@joelnordell joelnordell deleted the replacer-interface branch February 26, 2021 13:32
mittwillson pushed a commit to itering/gorm that referenced this pull request Sep 27, 2021
* Change NameReplacer to an interface, allowing custom Replacers.

* Add NoLowerCase option to skip the snake_casing of names.

* Move sync.Map from global variable into member of NamingStrategy.

This maintains backward compatibility by making the smap optional - the
NamingStrategy still works if it is nil. gorm.Open activates it by
calling Init() if the given Namer is a schema.NamingStrategy.

Also, this changes the key stored in the smap to be the original name,
instead of the replaced name.

* Refactor NamingStrategy tests to add more assertions about how and when Replacers get called.

* Remove the name cache from NamingStrategy.
cgxxv pushed a commit to cgxxv/gorm that referenced this pull request Mar 25, 2022
* Change NameReplacer to an interface, allowing custom Replacers.

* Add NoLowerCase option to skip the snake_casing of names.

* Move sync.Map from global variable into member of NamingStrategy.

This maintains backward compatibility by making the smap optional - the
NamingStrategy still works if it is nil. gorm.Open activates it by
calling Init() if the given Namer is a schema.NamingStrategy.

Also, this changes the key stored in the smap to be the original name,
instead of the replaced name.

* Refactor NamingStrategy tests to add more assertions about how and when Replacers get called.

* Remove the name cache from NamingStrategy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants