-
Notifications
You must be signed in to change notification settings - Fork 1
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 Controller and Service support #1
base: 6.0.x
Are you sure you want to change the base?
Conversation
@codeconsole This is good, I will check it out later. How about using Also, I prefer to use public @interface @Scaffold {
Class<?> value() default Void.class;
Class<?> base() default RestController.class;
} In addition, this new feature would be more appropriate in scaffodling What do you think? Have you tried using it like this?, I've been hoping for a little cooler syntactic sugar in Grails, Rails has a syntax like concerns, which feels great.
|
@rainboyan there is a reason behind the approach I chose.
Application.groovy @ScaffoldController(SecuredGenericController)
class Application {
} Which would configure ALL Scaffold Controllers to use
ScaffoldController(GenericAsyncController)
class UserController {
static scaffold = User
} |
I have some changes I am currently testing, but appear to be working: @ScaffoldController(RestController<User>)
class UserController {}
@ScaffoldService(GenericService<User>)
class UserService {}
@ScaffoldController(GenericController<User>)
class ImageController {}
@ScaffoldService(GenericService<Image>)
class ImageService {} The domain attribute on the annotation is still available and takes precedence, but you do not have to provide it. It can be set using generics. |
I don't understand how the Could you provide some complete examples to explain how it works? |
@rainboyan 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 For example, let's create a class GenericService<T> {
T get(Long id) {
// generic get logic
}
List<T> list(Map args) {
// generic list logic
}
Long count(Map args) {
// generic count logic. (My implementation takes a map so counts can be done based on parameters)
}
void delete(Long id) {
// generic delete logic
}
T save(T domain) {
// generic save logic
}
} Now all I have to do is have define a Service with the annotation and all those methods become implemented. Unlike @ScaffoldService(GenericService<User>)
class UserService {
User save(User user) {
user.modified = new Date()
super.save(user)
}
} |
I don't think it's feasible, but I haven't tried it.
There is really no example of a global configuration of the Groovy AST.
The use of static properties is still the majority of options at present. But Groovy and Grails lack a clear definition of this part of the property.
Scaffolding seems to be referring specifically to Controllers and Views at the moment
@Resource(uri='/books', superClass=SubclassRestfulController)
class Book {
String title
static constraints = {
title blank:false
}
}
Have you tried this, whether this will meet your needs? class UserController extends GenericAsyncController<User> {
static scaffold = User
}
If you come across a method that GORM doesn’t know how to implement, then you can provide an implementation by using an abstract class, and you can also override individual methods. interface IBookService {
Book getBook(Serializable id)
Date someOtherMethod()
}
@Service(Book)
abstract class BookService implements IBookService {
@Override
Date someOtherMethod() {
// impl
}
} |
That will not render scaffold views from the ScaffoldViewResolver and is way too verbose.
That is not valid code and will throw an exception. Scaffold controllers can not have super classes. @ScaffoldController(GenericAsyncController<User>)
class UserController {}
If that is true, what is this Service.groovy?
That Is waay to verbose and you can not call super inside 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
}
List<T> list(Map args) {
// implementation ...
}
Long count() {} // implementation ...
void delete(Serializable id) {} // implementation ...
T save(T instance) // // implementation ...
}
@ScaffoldService(GenericService<User>)
class UserService {}
@ScaffoldController(RestfulController<User>)
class UserController {}
@ScaffoldService(GenericService<Book>)
class BookService {
Book save(Book book) {
book.owner = request.user
super.save(user)
}
}
@ScaffoldController(RestfulController<Book>)
class BookController {}
@ScaffoldService(GenericService<Car>)
class CarService {}
@ScaffoldController(RestfulController<Car>)
class CarController {}
@ScaffoldService(value = PetService<Pet>, readOnly = true)
class PetService {}
@ScaffoldController(value = RestfulController<Pet>, readOnly = true)
class PetController {}
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 I created a example to demonstrate how to use super controller with a scaffolding controller. https://github.com/rainboyan/grace-scaffolding-demo @Resource(uri='/books', formats = ['html', 'json'], superClass=ScaffoldBookController)
class Book {
String title
static constraints = {
}
} class UserController extends SubclassRestfulController<User> {
static scaffold = User
UserDataService userDataService
UserController() {
super(User, false)
}
@Override
protected User createResource(Map params) {
return new User(params)
}
@Override
protected User createResource() {
User user = new User()
bindData user, getObjectToBind()
user
}
@Override
protected User queryForResource(Serializable id) {
return this.userDataService.get(id)
}
@Override
protected Integer countResources() {
return this.userDataService.count()
}
@Override
protected User saveResource(User resource) {
return this.userDataService.save(resource)
}
@Override
protected void deleteResource(User resource) {
this.userDataService.delete(resource.id as Serializable)
}
} |
@rainboyan I don't like it. There isn't a clear Separation of Concerns. The whole reason behind @ScaffoldController and @ScaffoldService is to keep code DRY while at the same time having a clear Separation of Concerns.
class ScaffoldBookController extends RestfulController<Book> {
static scaffold = Book
ScaffoldBookController(Class<Book> resource) {
super(resource)
}
ScaffoldBookController(Class<Book> resource, boolean readOnly) {
super(resource, readOnly)
}
} can be replaced with @ScaffoldController(RestfulController<Book>)
class BookController {} If you have 50 controllers, your version required maintaining a minimum of 200 more lines of code across 100 files.. If you are so pro RestfulController, maybe you should change Controller.groovy to <%=packageName ? "package ${packageName}" : ''%>
class ${className}Controller extends RestfulController<${className}>{
} |
Another option is since 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
}
List<T> list(Map args) {
// implementation ...
}
Long count() {} // implementation ...
void delete(Serializable id) {} // implementation ...
T save(T instance) // // implementation ...
}
@Scaffold(GenericService<User>)
class UserService {}
@Scaffold(RestfulController<User>)
class UserController {}
@Scaffold(GenericService<Book>)
class BookService {
Book save(Book book) {
book.owner = request.user
super.save(user)
}
}
@Scaffold(RestfulController<Book>)
class BookController {}
@Scaffold(GenericService<Car>)
class CarService {}
@Scaffold(RestfulController<Car>)
class CarController {}
@Scaffold(value = PetService<Pet>, readOnly = true)
class PetService {}
@Scaffold(value = RestfulController<Pet>, readOnly = true)
class PetController {}
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
}
} |
@rainboyan Here is what the final product would look like. Let me know if you want me to merge in the rest of the changes or you have any further suggestions. grails/scaffolding#118 |
@codeconsole I have updated the demo, does it meets your requirements? But currently it's not working, I will figure it out later, maybe it's related to the GORM side. class UserController extends ScaffoldRestfulController<User> {
static scaffold = User
UserDataService userDataService
UserController() {
super(User, userDataService)
}
}
abstract class ScaffoldRestfulController<T> extends RestfulController<T> {
CrudDataService<T> crudDataService
ScaffoldRestfulController(Class<T> domainClass, CrudDataService<T> crudDataService) {
this(domainClass, false, crudDataService)
}
ScaffoldRestfulController(Class<T> domainClass, boolean readOnly, CrudDataService<T> crudDataService) {
super(domainClass, readOnly)
this.crudDataService = crudDataService
}
@Override
protected T queryForResource(Serializable id) {
return this.crudDataService.get(id)
}
@Override
protected Integer countResources() {
return this.crudDataService.count()
}
@Override
protected T saveResource(T resource) {
return this.crudDataService.save(resource)
}
@Override
protected void deleteResource(T resource) {
this.crudDataService.delete(resource?.id as Serializable)
}
} I don't think scaffolding plugin should provide dynamic GORM services, although it provides Service template. Because the business logic is the detail, scaffolding plugin can't do much. The other thing is If you use Scaffolding in your application, the readOnly should always be false. |
Why should it always be false? What if you wanted to expose 2 scaffold controllers, 1 admin and 1 for developers as an api that should only be readonly? What if you have a data layer that doesn't accept writes? (I have such a case using a cloud datastore) Why do this: class UserController extends ScaffoldRestfulController<User> {
static scaffold = User
UserDataService userDataService
UserController() {
super(User, userDataService)
}
} when you can do this: @Scaffold(RestfulServiceController<User>)
class UserController {} or even this: @Scaffold(SecuredNonRestfulServiceController<User>)
class UserController {} which way is more DRY? your proposal or mine? why not provide both ways and give the developer a choice?
|
@codeconsole Partially accepted. See https://github.com/rainboyan/grace-scaffolding-demo/blob/main/README.md In order to make the two ways consistent in use. Only supports Scaffolding Controllers, maybe there will be a Scaffolding Service in the future. |
@rainboyan I am not a big fan of With the latest Grails you can now do the following: package website
@Scaffold(GormService<User>)
class UserService {}
package website.admin
@Scaffold(RestfulServiceController<User>)
@Secured('ROLE_ADMIN')
class UserController {
static namespace = 'admin'
}
package website
@Scaffold(value = RestfulServiceController<User>, readOnly = true)
class UserController {}
|
@codeconsole Thanks for your comments. class Post {
static hasMany = [comments: Comment]
static mapping = {
table: 'posts'
}
} The Groovy language uses annotations, mostly used in Groovy AST transformations, such as, import groovy.transform.ToString
@ToString
class Person {
String firstName
String lastName
} Spring and Micronaut are frameworks with Java as the main language, and of course they mostly use Java annotations. In the current version (Grace 2023.1.x), I don't plan to make major changes to the Scaffolding plugin, just to improve it (#2) Anyway, thank you for your PRs.It's a pleasure to discuss with you. |
correct, that is why Grails has numerous other annotations that also do AST transformations. Grails is built on top of Spring which now relies heavily on annotations.
static properties are legacy and poor design. They were not poor design when they were invented because annotations were not yet introduced. That is the only reason why they are there. I believe if Grails was created today, it would not use static properties at all. By choosing to use static properties over annotations, you are using design decisions of 2005.
The changes I propose to you are not major changes. Again, they are optional functionality that doesn't have to be used, but greatly empowers the developer if they choose to use them. They promote good design by separating the service layer from the web layer. You have done an amazing job with Grace, but the more you diverge from Grails the less likely you will gain adoption from the Grails community. It would be nice if Grace offered a seamless transition from Grails especially when Grails is starting to gain momentum again. People are hesitant to move a project to a new framework unless it offers non lock in and an escape route. It would also be amazing if you could contribute back into Grails some of the lessons you learned in Grace. |
I don't agree with you,
Sometime, only when your Grails app outside Grails
Grace Scaffolding 7.x will introduce the following ways to provide more configuration options, I think this's something you can‘t do with annotation. class UserController {
static scaffold = {
fields: {
exclude: ['id', 'createdDate']
}
}
}
Perhaps Grace is for those who have never used Grails before, and the key to winning users is to lift the heavy burden of the past and embrace the future.
It's nice to see Grails start over, but it's hard for me to have a lot of energy and interest in Grails, and I have a lot of new ideas to work on Grace. |
How so? Then why are all the modifications being done with @AstTransformer ?
Isn't that behavior already available on the domain class? Why can't an annotation do the same thing?
I recommend you contribute in a way that promotes your own framework. Contribute to Grails things that would make it easier for Grails users to transition to Grace. You have the opportunity, especially now (before 7.0 is released), to do that. The more commonality between code bases, the more you can leverage commits/plugins from other people. There should be some inter compatibility between the 2 frameworks. Take Apache Cordova for instance. It is kind of like Grails. A lot of legacy code and a huge community of plugins. Then look at Capacitor. Completely rewritten and amazing and all the plugins of Cordova run under Capacitor. It is an easy transition and Capacitor is well maintained. I want you to succeed. I would love to migrate some of my apps to Grace, but right now it is not an easy task. It is easier for me to focus on making Grails better. Grace should extend the capabilities of Grails. Also, by contributing beneficial improvement to Grails, you gain more attention from the Grails community which is free adverstising to your framework. |
Annotations can be simple, but complex situations are a bit inappropriate. Scaffold clearly needed a more flexible and complex design.
In my opinion, migrating from Grails to Grace is really feasible, given the size and complexity of the project's code. I don't recommend upgrading directly, as it requires a specific analysis of how many external plugins are used in the project, and in my opinion, most of them are unnecessary, they are not migrated to Spring Boot, or they can be replaced.
A lot of Grace plugins, Htmx, Hotwire, Policy, View Components will support both Grails and Grace.I think it's also a very good way to contribute to the Grails community.
Since the release of Grails to 3.0, the transition would have been a success. But it was a shame that we didn't take the opportunity to grow with Spring Boot. Since then, technical debt has been holding back its growth, and the development team has not had the motivation or determination to push for the refactoring and upgrading of Grails, which has been abandoned by the community. Grace has been actively refactoring and improving these from the start, which may lead to a parting of ways with Grails.
As I've said before, the gap between Grails and Spring Boot is getting bigger and bigger, and it's time to break through. Next, I'll update the documentation, Guides, and I'll be happy to talk to the Grails team and share some of my views. Thank you very much for your suggestions and contributions. |
Adds support for the annotation
@ScaffoldController
which provides an alternative to thestatic scaffold = Domain
approach.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.grails/scaffolding#113