Grails URL mappings - grails

In my Grails app I have a couple of domain classes, say Author and Book. I'm using the default URL mappings:
static mappings = {
"/$controller/$action?/$id?"{
constraints {
// apply constraints here
}
}
}
So the relative URL to show the book with id 2 is /book/show/2 or for the author with id 5 it's /author/show/5. Both the Author and Book classes have a unique name property.
I would prefer the URLs to show an author or book use the name rather than the database ID to identify the book/author, e.g. /author/show/shakespeare and /book/show/ulysses. Is this achievable without changing the domain classes such that the name field is the id (primary key)?

You could just pass strings (e.g. "shakespeare") as params.id and then instead of doing a:
Author.get(params.id)
in your controller method, do:
Author.findByName(params.id)
As far as I know, the id parameter (when used with URLs) doesn't have to be a long.

Related

Grails data won't bind if the id column is renamed

Grails will create an id and a version columns from a domain class automatically. I want to use my own column for the primary key. So, I follow the doc to change the mapping.
class book {
String isbn
static mapping = {
id generator: 'assigned', name: 'isbn'
}
}
So far so good. The isbn column is now the primary key.
I use generate-all to create the view and controller. However, the data binding won't work anymore.
Create and Save work no problem. It binds a book to the view. I can add a new book to the database no problem.
def create() {
respond new Book(params)
}
def save(Book book) {
if (book == null) {
notFound()
return
}
...
}
But the Update action does not bind. book is null after I click the Update button from the Edit view.
def update(Book book) {
if (book == null) {
notFound()
return
}
...
}
The codes generated by generate-all in the Save and Update actions are the same. I don't understand why it will bind the book to the Save action but not to Update action.
Would you show me the problem please?
Many Thanks!
I think I figure it out. When I bind an object to a view, Grails is hardcoded to look for the id property. It has to be spelled "id". If there is no "id" property in the domain class, Grails will not bind.
The way I figure this out is to look at the actual HTML generated by the server.
If there is an id property bind to the view, I see the HTML has the ../controller/action/id link.
If the id property is missing, the HTML link is just ../controller/index
I am new to Grails. So, I guess in order for the binding to work, I need to have an id property for Grails to put in the link.
I think this is a REST call. I don't know what REST is though.
So, I will try to add an dummy id property to my Book domain class to see if Grails will take the bait. I will set up the domain so Grails won't generate the id column in the database table. The id property is used locally only. No need to save it to the database.
class book {
String isbn
String id
static mapping = {
id generator: 'assigned', name: 'isbn'
}
}
I will copy the isbn value to the id property. I am not sure if this will work or not. I hope Grails will generate the link in the view with the isbn string in the id property instead of the default integer id value.
../controller/action/978-3-16-148410-0

Operation save in Grails on a POGO

I'm beginner in Grails and I have a problem when I try to save a POGO
I have created 1 domain class
class Book {
String title
}
Then, I have generated the controller and view automatically.
Now, I want to be able to create a book with the code by clicking "create" (I know it is possible directly with the code generated but for my example I want to do it by the code). To do this, I have modified the method 'save(Book bookInstance)' in the controller like this
#Transactional
def save(Book bookInstance) {
def book = new Book(title:"New Grails Book").save()
But, when I go to the URL localhost:8080/myApp/book/create and then I click "Create", I have the error
message -> /myApp/WEB-INF/grails-app/views/book/save.jsp
description -> The requested resource is not available.
When I put this code in bootStrap, it is OK, so I don't understand why it is not in the controller
When you have a hasMany property in a domain class, Grails adds a Set property to the domain class with an AST transformation (so it's actually there in the bytecode, and it's visiable to Java) to represent the collection, and when you add a belongsTo a field of that type is added. So it's as if you had this code:
class Author {
Set<Book> books
static hasMany = [books: Book]
String name
}
and
class Book {
Author author
static belongsTo = [author: Author]
String title
}
The AST xform uses the map key as the field name, so you can use any valid field name, but the convention is to do what you did.
Properties are nullable:false by default, so your code doesn't save the Book instance because you didn't set the author property. When doing this explicitly you typically don't create the Book directly, but instead add it to the Author's collection using the dynamic addToBooks method. This sets the author field back-reference and when you save the author, the book is transitively validated and saved. This is all handled for you when you have code like new Book(params).save(), and you can do it directly, e.g.
Author author = ...
def book = new Book(title:"New Grails Book", author: author).save()
If you're using a generated controller and GSPs, there should be an author id in the params map, it'll likely be author.id, so that first line would be
Author author = Author.get(params['author.id'])
but you can add
println params
at the top of the action method to see all of the submitted params.
In general you don't want to look at the return value of the save call, since it will be null if there's a validation error and there's no way to retrieve the errors. So change
def book = new Book(...).save()
to
def book = new Book(...)
book.save()
and now you can call book.hasErrors(), book.getErrors(), book.errors, etc. to see if it was successful and if not, what went wrong.
But that's not the exact problem you're seeing, just one you will when you fix your problem. There's no save.gsp, and Grails also looks for save.jsp and confusingly includes that name in the not-found message. The save method is accessed via a POST request, typically from the form generated by the create action, and it either re-displays create.gsp with the submitted data and error messages when validation fails, or redirects to the view action when the save succeeds. There's no need for a save.gsp when using the generated code.

Grails: display linked table value in list.gsp instead of id

Let me preface this ask by indicating that this only applies to /list/ view and NOT other views. This is NOT about drop-down menus
I have two tables:
[Authors]
- id
- name
[Books]
- id
- title
- author
In the Books form (create/edit) I can get the "author" field to display an author's name (from Author table instead of id) without an issue, however, when I view the /list/ view, it displays the author's id instead of the name (correctly, since this is what's stored in the DB).
I have not been able to find a method to convert "id" to "name" in the /list/ view elsewhere and I haven't used grails in almost a year (yay rust). Thanks in advance for guidance.
Try this:
class Author {
String name
static mapping = {
id name: 'name'
}
}
If you're using static scaffolding, don't forget to regenerate the views for the Author and Book domain classes:
grails> generate-views com.yourPackage.Author
grails> generate-views com.yourPackage.Book

Grails domain ID column validation issue

I have a simple domain object
class MyDomain
{
String id
String name
static constraints =
{
id unique:true
name nullable:true
}
static mapping =
{
table 'schema.MyDomain'
id column:'MY_ID', type:'string', generator:'assigned'
}
}
The issue I am having is when I call validate on the object, it returns true even when the id field is null. I had thought that all columns were nullable:false unless explicitly stated otherwise. If I change the line
id unique:true
to
id unique:true, nullable:false
then it seems to work fine. My main question is, why do I have to explicitly set nullable for the ID column? It is just a small line of code, but I don't like just adding in the tag of code without understanding why in case it is a symptom of a bigger problem.
The id column is auto generated and auto populated(when versioning is turned on[true by default]) and you shouldn't have to declare a new one.
This default id column is nullable:false by default and you can still set the mapping properties and id generation strategies like you have done above against it.
However if you want to define the default constraints for all domain in you app, you can do it globally by setting thwe following in your config.groovy file.
grails.gorm.default.constraints = {
myShared(nullable:true, size:1..20)
}
For more on constraints see the Grails documentation.

Wordpress like dynamic permalinks in ASP.NET MVC2/3 or ASP.NET 4.0

Scenario:
There are two entities say 'Books' and 'Book Reviews'. There can be multiple books and each book can have multiple reviews.
Each review and book should have a separate permalink. Books and Reviews can be added by users using separate input forms. As soon as any book/review is added it should be accessible by its permalink.
Anyone can point me in the right direction on how should this be implemented?
Url routing will handle this out of the box with no additional real work required.
Just create a Books or BookReviews controller.
Create a action method that takes an Id
You will have basic permalinks like
/Books/Details/1
and
/BookReviews/Details/4
If your happy with permalinks like that, then you are good to go. :)
However, if you want to take it further and make it even more search engine friendly with a little bit more work......
First you should create or find a simple "slug" encoder, which will take a string (perhaps the title of the book or reviewer) and encode any non-alphanumeric characters into a - or similar. URL Slugify algorithm in C#?
Now we can create a route like
Books/{title}-{id}
routes.MapRoute(
"BooksSeoRoute",
"Books/{slug}-{id}",
new { controller = "Books", action = "Details" } // Parameter defaults
);
So we end up with permalinks that look like:
Books/The-Title-Of-The-Book-38
For the book reviews, you might want to have a "nested" approach so you could use
routes.MapRoute(
"BookReviewsSeoRoute",
"Books/{book-slug}/{slug}-{id}",
new { controller = "BookReviews", action = "Details" } // Parameter defaults
);
Books/The-The-Of-The-Book-38/Review-by-John-Smith-24
You can either add a slug field to your models/entites/database, or you can just add a getter that will dynamically generate it from your title/author
ie
public class Book {
public string Id {get;set;}
public string Title {get;set;}
public string Slug
get
{
return SlugEncoder.EncodeString(this.Title);
}
}
In your views
=Html.ActionLink("Permalink","Details","Book",new{#Id=Model.Id, #Slug=Model.Slug})

Resources