【Grails 4.0】Grails - 使用Apache Groovy編程語言的開源Web應用程序框架

Grails是一種根基於Groovy, Spring frameworkHibernate的 Web Framework,也就是說,它把所有要開發網站所需要用到的功能都結合起來了,照官方的說法是 - 『A powerful Groovy-based web application framework for the JVM built on top of Spring Boot』,因為公司需要用到,所以在這裡記錄一下。身為單身狗的我,祝大家情人節快樂,請上帝賜給我一個可愛的正妹吧。

安裝

安裝SDKMAN!

  • SDKMAN!是一款管理SDK版本的工具,可以用於大多數基於Uinx的系統。它提供了命令行以及API,功能有:安裝、移除、列舉候選版本。它跟Homebrew滿像的,但是好像都只支援JVM系的SDK?
curl -s "https://get.sdkman.io" | bash
sdk version

安裝JDK

sdk list java                       # 查看支援的版本
sdk install java                    # 安裝最穩定的版本
sdk install java 11.0.10.hs-adpt    # 安裝特定版本
java --version                      # 查看Java的版本

安裝Grails

sdk list grails             # 查看支援的版本
sdk install grails          # 安裝最穩定的版本
sdk install grails 4.0.7    # 安裝特定版本
grails --version            # 查看Grails的版本

查看當前安裝的版本

  • 當然也可以查看當前安裝的版本
sdk current java
sdk current grails

切換當前版本

  • 當然,這一類的工具最重要的就是要有『切換版本』的功能
sdk list java
sdk install java 8.0.282.hs-adpt
sdk default java 8.0.282.hs-adpt

使用Grails

建立空白資料夾

  • 首先先建立一個空白的資料夾,然後進入Grails CLI,建立Web-APP
cd ~/Desktop
md grails_demo          # 建立空白資料夾
cd grails_demo
grails                  # 啟動Grails CLI
create-app              # 建立Web-APP

啟動Web-APP

  • Grails CLI內,啟動Web-APP,完成後,就可以在這裡看到Grails的啟動畫面了
run-app                 # 啟動Web-APP
run-app -port=8787      # 建立Web-APP (特定Port)

關閉Web-APP

  • 有啟動當然就有關閉
stop-app
exit

萬一把Termail關掉怎麼辦?

  • 有時候會不小心Terminal關掉,但是Grails還在Run,那要怎麼關呢?沒關係,還是有辦法的,利用lsof + kill大法
lsof -i:8080    # 查看8080Port是哪些程式在使用的
kill <PID>      # 關閉該程式 with <PID>

Grails的第一課

Grails檔案長相

BootStrap

  • Grails啟動設定
package grails_demo

class BootStrap {

    def init = { servletContext ->
        this.initSetting()
    }
    
    def destroy = {}

    private def initSetting() {
        def message = "==> The Message is ${this.message()}"    // 可以用def當成回傳型態 => Object (Grails)
        println(message)
    }

    private String message() {
        String message = 'Grails is Good !!!'                   // 也可以直接用String當成回傳型態 => String (Java)
        return message
    }
}

建立Controller

  • 建立一個名叫『first』的『Controller』,位置在『grails-app/controllers/grails_demo/FirstController.groovy』,這個跟iOS的ViewController很像,可以用Code寫HTML,網址就是http://localhost:8080/first/index
create-controller first
package grails_demo

class FirstController {

    def index() { 
        render('<h1>Grails初體驗</h1>')
    }
}

package grails_demo

class FirstController {

    /// index()就是指grails-app/views/first/index.gsp => http://localhost:8080/first/index
    def index() { 
        // render('<h1>Grails初體驗</h1>')
    }

    /// other()就是指grails-app/views/first/other.gsp => http://localhost:8080/first/other
    def other() {}
}

建立Domain

Create-Domain-Class

create-domain-class myBook
run-app

連接Database Console

jdbc:h2:mem:devDb

設定資料屬性

  • 這裡就來新增一些欄位,跟相關屬性,記得要重開Web-APP才可以生效
  • 不得不說,這一類的ORM真的太好用了,在這裡叫做GORM - Grails Object Relational Mapping,不用去學SQL語法,就可以簡單入門,雖然不能取代SQL,但在基本的應用上已經可以解決大部分的問題了
package grails_demo

class MyBook {

    static constraints = {
        title unique: true
        releaseDate nullable: true
    }

    String title
    Date releaseDate
}

新增資料

  • GORM的第一步就先新增一筆資料吧,順便也使用一下簡單的GSP Tags
  • 利用[http://localhost:8080/first/index?title=重新認識 Vue.js](http://localhost:8080/first/index?title=重新認識 Vue.js)就可以新增一本書籍的資料到資料庫上
package grails_demo

class BootStrap {

    def init = { servletContext ->
        this.initSetting()
    }
    
    def destroy = {}

    private def initSetting() {
        TimeZone.setDefault(TimeZone.getTimeZone('UTC')) 
    }
}
package grails_demo

import grails.artefact.Controller
import java.time.*

class FirstController {

    /// index()就是指grails-app/views/first/index.gsp => http://localhost:8080/first/index
    def index() {
        def title = this.queryString('title', this)
        def result = this.appendBook(title, new Date())
        return [result: result]
    }

    /// 新增書籍
    private def appendBook(String title, Date releaseDate) {
        
        def book = new MyBook()

        book.title = title
        book.releaseDate = releaseDate

        return book.save()
    }

    /// 取得網址上的queryString
    private def queryString(String key, Controller self) {
        def queryString = self.getParams()
        return queryString[key]
    }
}
<!doctype html>
<html>
<head>
    <title>Grails初體驗</title>
</head>
<body>
    <g:if test="${result}">
        <h1>新增成功 - ${result.title}</h1>   
    </g:if>
    <g:else>
        <h1>新增失敗</h1>   
    </g:else>
</body>

安裝好用的IDE

為什麼不用VSCode?

  • 寫後臺不不外乎就是CRUD - 新增 / 修改 / 刪除 / 查詢,因為Grails會自動產生一些相關的方法,所以在點點點的時候,希望能自帶方法出來,所以要安裝IDE。

Spring Tool Suite

IntelliJ IDEA

安裝IntelliJ-IDEA

java --version
brew install --cask intellij-idea

安裝plugin

dependencies {
    compile 'org.grails.plugins:excel-import:3.0.2'
    runtime 'mysql:mysql-connector-java:8.0.23'
}

切換專案環境JDK

  • 不得不說,IDE最重要的功能之一,就是切換SDK版本,因為有時候要換到舊版本才能跑

資料庫的操作

常用小工具

  • 基本的新增功能,因為GORM的關係,只要使用save()就可以把資料存到資料庫了
  • 這裡新增一個ApiController,用來放置API使用
  • 另外個人習慣寫一些小工具,就放在utils資料夾中的package中
package idv.william

import grails.artefact.Controller
import java.text.SimpleDateFormat

// MARK: - 小工具
final class Utility {

    // MARK: - 網頁相關
    /// 取得網址上的queryString
    def queryString(String key, Controller self) {
        def queryString = self.getParams()
        return queryString[key]
    }

    /// ["2021-01-01 01:23:45" => 1577836800](https://timestamp.online/)
    def dateStringToTimestamp(String dateString, Enumeration.DateFormat pattern) {

        if (dateString == null || pattern == null) { return null }

        def format = new SimpleDateFormat(pattern.toString())
        def timestamp = format.parse(dateString).getTime() / 1000

        return timestamp
    }

    /// [2021-01-01 01:23:45 => 1577836800](https://timestamp.online/)
    def dateToTimestamp(Date date, Enumeration.DateFormat pattern) {

        if (date == null || pattern == null) { return null }

        def dateString = this.dateToString(date, pattern)
        def timestamp = this.dateStringToTimestamp(dateString, pattern)

        return timestamp
    }

    /// "2020-01-01" => 2021-01-01 01:23:45
    def stringToDate(String dateString, Enumeration.DateFormat pattern) {

        if (dateString == null || pattern == null) { return null }

        def format = new SimpleDateFormat(pattern.toString())
        def date = format.parse(dateString)

        return date
    }

    /// 2021-01-01 01:23:45 => "2021-01-01"
    def dateToString(Date date, Enumeration.DateFormat pattern) {

        if (date == null || pattern == null) { return null }

        def format = new SimpleDateFormat(pattern.toString())
        def dateString = format.format(date)

        return dateString
    }
}
package idv.william

/// MARK: - 常用列舉
final class Enumeration {

    /// [日期格式](http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns)
    enum DateFormat {

        Short("yyyy-MM-dd"),
        Middle("yyyy-MM-dd HH:mm"),
        Long("yyyy-MM-dd HH:mm:ss"),
        Full("yyyy-MM-dd HH:mm:ss ZZZ")

        private final String value

        /// 利用文字尋找Enum
        static find(String value) { values().find { it.value == value } }

        /// 初始化
        DateFormat(String value) { this.value = value }

        /// override toString() => 取得內容文字的值
        String toString() { return value }
    }
}

新增書籍API

  • 寫完後,可以試試[API](http://localhost:8080/api/appendBook?title=重新認識Vue.js:008天絕對看不完的Vue.js 3指南&releaseDate=2021-02-09),是不是簡單又好用啊?終於可以轉生成為C/P值極高的高端打字工了。
package grails_demo

import grails.converters.JSON
import idv.william.Enumeration
import idv.william.Utility

final class ApiController {

    private def util = new Utility()

    def index() {}

    // MARK: - API
    /// 新增書籍
    /// => http://localhost:8080/api/appendBook?title=重新認識Vue.js:008天絕對看不完的Vue.js 3指南&releaseDate=2021-02-09
    /// => http://localhost:8080/api/appendBook?title=就算忙盲茫 我決定給自己一點時間&releaseDate=2021-03-02
    /// => http://localhost:8080/api/appendBook?title=天橋上的魔術師&releaseDate=2011-11-30
    def appendBook() {

        def title = util.queryString('title', this)
        def releaseDate = util.queryString('releaseDate', this)
        def json = this.appendBookResult(title, releaseDate)

        render(json)
    }

    // MARK: - Function
    /// 新增書籍
    private def appendBookResult(String title, String releaseDate) {

        if (title == null) {
            def json = [error: '沒打書名'] as JSON
            return json
        }

        if (releaseDate == null) {
            def json = [error: '沒打發行日期'] as JSON
            return json
        }

        def myBook = new MyBook()

        myBook.title = title
        myBook.releaseDate = util.stringToDate(releaseDate, Enumeration.DateFormat.Short)

        def result = myBook.save()                          // 將資料儲存到資料庫中
        def json = [result: result] as JSON                 // 將Map => JSON

        return json
    }
}

搜尋書籍API

  • 在這裡,我們以id做為搜尋的依據,就是findBy系的function,是不是很方便啊?
final class ApiController {
    
    /// 搜尋書籍 => http://localhost:8080/api/searchBook?id=3
    def searchBook() {

        def id = util.queryString('id', this) as Long
        def book = MyBook.findById(id)

        render(book.title)
    }
}

修改書籍API

  • 在這裡,還是以id做為修改的依據 - 唯一值,加上要修改的title內容,簡單來說,就是『搜尋 => 設定數值 => 記錄』,不過在這裡要注意的是,要加上@Transactional這個Tag,不然的話,實際上在資料庫是沒有改的。
package grails_demo

import grails.gorm.transactions.Transactional

final class ApiController {
    
    /// 修改書籍 => http://localhost:8080/api/editBook?id=3&title=天橋上的魔術師ABC
    @Transactional
    def editBook() {

        def id = util.queryString('id', this) as Long
        def title = util.queryString('title', this) as String
        def myBook = MyBook.findById(id)

        myBook.title = title

        def result = myBook.save()
        def json = [result: result] as JSON

        render(json)
    }
}

刪除書籍API

  • 最後,還是以id做為刪除的依據,就是『搜尋 => 刪除』,不過在這裡要注意的是,還是要加上@Transactional這個Tag,不然的話在資料庫還是刪不掉的。
package grails_demo

import grails.gorm.transactions.Transactional

final class ApiController {
    
    /// 刪除書籍 => http://localhost:8080/api/deleteBook?id=3
    @Transactional
    def deleteBook() {

        def id = util.queryString('id', this) as Long
        def title = util.queryString('title', this) as String
        def myBook = MyBook.findById(id)

        myBook.delete()

        render("刪除完成")
    }
}

範例程式碼下載

  • 如果有遇到grails不能執行的情況,請將build資料夾刪除,再執行即可

後記

  • 學這個能在台灣找工作嗎?我上1111人力銀行搜尋了一下,發現…很可怕,居然完全沒有相關的工作,應該是它寫的搜尋法太差了吧?還是說這個工作太穩了?XD