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

@Scaffold annotation for Controllers and Services #118

Merged
merged 13 commits into from
Sep 10, 2024

Conversation

codeconsole
Copy link
Contributor

@codeconsole codeconsole commented Sep 2, 2024

Adds support for the annotation@Scaffold which provides an alternative to the static scaffold = Domain approach.

@Scaffold(domain = User)
class UserController {}

It also empowers the developer to use their own base controller instead of being forced to use RestController which could be very limiting. RestController does not encapsulate all necessary business logic and is completely divergent from the Service MVC model that is generated by this plugin. With this annotation, developers can now use an extended RestController to fit their needs, or create an entirely different generic controller that is not related at all to RestController and more closely matches the type of controller that is generated by this plugin.

@Scaffold(RestfulServiceController)
class UserController {
    static scaffold = User
}
@Scaffold(value = RestfulServiceController, domain = User)
class UserController {}

can now be written as this

@Scaffold(RestfulServiceController<User>)
class UserController {}

Introduce @Scaffold Service support

It is very powerful if you have common business logic shared across multiple or all services and use a service layer. It works similar to how scaffold controllers work. You have a super class that you define (such are RestfulContoller for scaffold controllers) and then any Services with the annotation get that super class injected.

Now all I have to do is have define a Service with the annotation and all those methods become implemented. Unlike @Service, I can override individual methods and call super

@Scaffold(GormService<User>)
class UserService {

    User save(User user) {
         user.modified = new Date()
         super.save(user)
    }
}

Introduced 2 new classes:
RestfulServiceController - which is the same as RestfulContoller, but makes all datasource calls to the respective Service.
GormService - which scaffolds a generic service similar to Service.groovy but instead of using an interface with @Service, it sets a superclass where methods can be overridden and extended. You can also define any class as your base service.

By using @Scaffold you can now do things for specific needs of groups of domain objects.

@Scaffold<SecuredAdminController<User> // adds @Secured('ROLE_ADMIN') to all methods
@Scaffold<domain = User, readOnly = true> // puts RestfulContoller in readOnly mode
@Scaffold<CachingService<User> // uses Redis as a caching layer 
@Scaffold<SearchableService<User> // indexes changes to Elastic search

The possibilities are endless!

#113

#114

@matrei
Copy link
Contributor

matrei commented Sep 2, 2024

Looks awesome!

Is using:

@ScaffoldService(GenericService<User>)
class UserService
{}

equivalent to:

class UserService extends GenericService<User>
{}

It would be really nice if there were some tests showing that these new features work as intended.

@codeconsole
Copy link
Contributor Author

codeconsole commented Sep 3, 2024

Looks awesome!

Is using:

@ScaffoldService(GenericService<User>)
class UserService
{}

equivalent to:

class UserService extends GenericService<User>
{}

yes, it is equivalent more or less. It works the same way how static scaffold = User works where UserController ends up extending RestController

The main work that it does is by setting the domain class in the constructor. If you used class extension, you would have to create a constructor for every service..

class UserService extends GenericService<User> {
     UserService() {
          super(User, false)
     }
}

would more or less be the equivalent.

I am trying to decide whether or not to ditch the readOnly property. If it is going to be used, it should be added to the annotation. I am thinking about adding readOnly to the @ScaffoldController annotation

@matrei
Copy link
Contributor

matrei commented Sep 3, 2024

If you used class extension, you would have to create a constructor for every service.

I don't follow, why would I have to create a constructor (if my GenericService has a no-args constructor)?
What is the false value you are passing to the constructor in your example? readOnly?

@codeconsole
Copy link
Contributor Author

codeconsole commented Sep 3, 2024

If you used class extension, you would have to create a constructor for every service.

I don't follow, why would I have to create a constructor (if my GenericService has a no-args constructor)?

If your GenericService has a no-args constructor, how else would you resolve as a User if you don't have a reference to T.class?

class GenericService<T> {
    @Autowired DatastoreService datastore

    GenericService() {}

    T get(Serializable id) {
        // how else would you resolve T ???
    }
}

Doesn't GenericService need a reference to the class it is revolving?

@ScaffoldService(GenericService<User>)
class UserService {}

vs

class UserService extends GenericService<User> {
     UserService() {
          super(User, false)
     }
}
class GenericService<T> {
    Class<T> resource
    String resourceName
    @Autowired DatastoreService datastore

    GenericService(Class<T> resource, boolean readOnly) {
        this.resource = resource
        this.readOnly = readOnly
    }

    T get(Serializable id) {
        T instance = resource.getDeclaredConstructor().newInstance()
        instance.properties << datastore.get(id)   
        instance
    }

What is the false value you are passing to the constructor in your example? readOnly?

yes, readOnly.

@matrei
Copy link
Contributor

matrei commented Sep 3, 2024

If your GenericService has a no-args constructor, how else would you resolve as a User if you don't have a reference to T.class?

Can't you get the implementation class with getClass():

T get(Serializable id) {
    T instance = (T) getClass().getDeclaredConstructor().newInstance()
    instance.properties << datastore.get(id)
    instance
}

@codeconsole
Copy link
Contributor Author

If your GenericService has a no-args constructor, how else would you resolve as a User if you don't have a reference to T.class?

Can't you get the implementation class with getClass():

T get(Serializable id) {
    T instance = (T) getClass().getDeclaredConstructor().newInstance()
    instance.properties << datastore.get(id)
    instance
}

Isn't the whole point to instantiate and return aUser instance? Wouldn't your code getClass().getDeclaredConstructor().newInstance() just create another UserService??

@matrei
Copy link
Contributor

matrei commented Sep 3, 2024

Isn't the whole point to instantiate and return aUser instance? Wouldn't your code getClass().getDeclaredConstructor().newInstance() just create another UserService??

Yes, you are correct! Thinking mistake by me 😄
Then, I see the use of the @ScaffoldService annotation.

@codeconsole
Copy link
Contributor Author

Isn't the whole point to instantiate and return aUser instance? Wouldn't your code getClass().getDeclaredConstructor().newInstance() just create another UserService??

Yes, you are correct! Thinking mistake by me 😄 Then, I see the use of the @ScaffoldService annotation.

All good. You almost had be believing you when I read your code at 2am lol

@codeconsole
Copy link
Contributor Author

codeconsole commented Sep 7, 2024

@matrei @jamesfredley I am getting close to merging this. One last thing we could consider:
Since @ScaffoldController and @ScaffoldService are identical, should we use the same annotation @Scaffold for both controllers and services?

They both do the same thing:

  1. define a super class.
  2. define a domain class.
  3. define readOnly mode.
@Scaffold(GenericService<User>)
class UserService {}
@Scaffold(RestfulController<User>)
class UserController {}
@Scaffold(domain = Book)
class BookController {}

@matrei
Copy link
Contributor

matrei commented Sep 7, 2024

@codeconsole If it is technically feasible, I think a unified @Scaffold annotation sounds excellent.

Should we also deprecate (not remove), the usage of static scaffold = Book, to signal that the annotation is the new better way. static scaffold = Book is also not working on Services and could therefore cause confusion.

@jamesfredley
Copy link
Contributor

jamesfredley commented Sep 7, 2024

@Scaffold would be ideal.

And we will want to update https://github.com/grails/grails-doc/blob/6.2.x/src/en/guide/scaffolding.adoc

@codeconsole
Copy link
Contributor Author

Provided default GenericService implementation that matches Service.groovy, but allows method overriding and calls to super:

@Scaffold(domain = User)
class UserService {}

@Scaffold(domain = Book)
class BookService {
    Book save(Book book) {
         book.owner = request.user
         super.save(user)
    }
}

@Scaffold(domain = Car)
class CarService {}

@Scaffold(domain = Pet, readOnly = true)
class PetService {}

class BootStrap {
    CarrService carService
    PetService petService
    UserService userService
    BookService bookService

    def init = { servletContext ->
         User rainboyan = userService.save(new User(username:'rainboyan')
         Car porsche = carService.save(new Car(make:'Porsche',  model:'911', owner: rainboyan)
         Book grails = bookService.save(new Book(name:'Programming Grails')
         Pet cat = petService.save(new Pet(name: 'Garfield', owner: rainboyan) // exception throw because service is readOnly
    }
}

@codeconsole codeconsole changed the title @ScaffoldContoller and @ScaffoldService support @Scaffold annotation for Controllers and Services Sep 7, 2024
@codeconsole
Copy link
Contributor Author

codeconsole commented Sep 7, 2024

Created a version of RestfulController that utilizes services so that a complete implementation could be provided that uses a scaffolded service.

@Scaffold(RestfulServiceController<Book>)
class BookController {}
@Scaffold(domain = Book)
class BookService {}

@matrei
Copy link
Contributor

matrei commented Sep 8, 2024

Could we compile GenericService statically if we narrow the type to <T extends GormEntity> and create an instance in the constructor for the GormAllOperations methods?

And don't we also have to manage the readOnly property for the save and delete methods, and also set @Transactional on those two methods?

package grails.plugin.scaffolding

import grails.artefact.Artefact
import grails.gorm.api.GormAllOperations
import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import grails.util.GrailsNameUtils
import groovy.transform.CompileStatic

@Artefact("Service")
@ReadOnly
@CompileStatic
class GenericService<T extends GormEntity<T>> {

    GormAllOperations<T> resource
    String resourceName
    String resourceClassName
    boolean readOnly

    GenericService(Class<T> resourceClass, boolean readOnly) {
        this.resource = resourceClass.getDeclaredConstructor().newInstance() as GormAllOperations<T>
        this.readOnly = readOnly
        this.resourceClassName = resourceClass.simpleName
        this.resourceName = GrailsNameUtils.getPropertyName(resourceClass)
    }

    protected T queryForResource(Serializable id) {
        resource.get(id)
    }

    T get(Serializable id) {
        queryForResource(id)
    }

    List<T> list(Map args) {
        resource.list(args)
    }

    Long count() {
        resource.count()
    }

    @Transactional
    void delete(Serializable id) {
        if (readOnly) {
            return
        }
        resource.delete(queryForResource(id), [flush: true])
    }

    @Transactional
    T save(T instance) {
        if (readOnly) {
            return instance
        }
        resource.save(instance, [flush: true])
    }
}

@codeconsole
Copy link
Contributor Author

Could we compile GenericService statically if we narrow the type to <T extends GormEntity> and create an instance in the constructor for the GormAllOperations methods?

And don't we also have to manage the readOnly property for the save and delete methods, and also set @Transactional on those two methods?

Sure, should we rename GenericService to something else? GormService or GenericGormService ?

@matrei
Copy link
Contributor

matrei commented Sep 8, 2024

Sure, should we rename GenericService to something else? GormService or GenericGormService ?

Maybe DefaultScaffoldService

@codeconsole
Copy link
Contributor Author

Sure, should we rename GenericService to something else? GormService or GenericGormService ?

Maybe DefaultScaffoldService

but is this the best name since it could also be utilized independently like RestfulController can?

Copy link
Contributor

@jamesfredley jamesfredley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GormService is a unique name across the Grails codebase. 👍

Issue created for documentation update: grails/grails-doc#909

@codeconsole codeconsole merged commit 279d70a into grails:5.1.x Sep 10, 2024
3 checks passed
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.

3 participants