多對多關聯在 Web 應用中常見且棘手。本文以行程規劃器為例,示範 Grails 如何透過 GORM 優雅建模多對多關係,並以 AJAX 提升大量選項下的使用者體驗。
class Airline {
static hasMany = [flights: Flight]
String name, iata, frequentFlier
}
class Flight {
static belongsTo = [trip: Trip, airline: Airline]
String flightNumber
Date departureDate, arrivalDate
Airport departureAirport, arrivalAirport
}
belongsTo 為 Map 形式可建立多重單向關聯;iata 儲存 IATA 代碼(如 UAL、DAL)。
class Airport {
static hasMany = [flights: Flight]
String name, iata, city, state, country
}
Flight 以 departureAirport/arrivalAirport 明確關聯,**不使用 belongsTo**,故刪除機場不連帶刪除航班,體現鬆耦合設計。
MySQL 實際產出 4 張表:
mysql> show tables;
+----------------+
| Tables_in_trip |
+----------------+
| airline |
| airport |
| flight | -- 連接表
| trip |
+----------------+
flight 表含 trip_id、airline_id、departure_airport_id、arrival_airport_id 等外鍵,完美對應領域模型。

<g:select optionKey="id"
from="${Airline.list()}"
name="airline.id"
value="${flight?.airline?.id}" />
渲染為標準 <select>,選項值為 airline.id,顯示文字為 toString() 結果。
全球機場數千,下拉選單不可用。IATA 代碼(如 DEN、ORD)為業界通用識別碼,適合作為輸入介面。
Controller 端:回傳 JSON
import grails.converters.*
class AirportController {
def scaffold = Airport
def getJson = {
def airport = Airport.findByIata(params.iata)
if (!airport) {
airport = new Airport(iata: params.iata, name: 'Not found')
}
render airport as JSON
}
}
測試:https://localhost:9090/trip/airport/getJson?iata=den
{"id":1,"class":"Airport","city":"Denver","country":"US","iata":"DEN","name":"Denver International Airport","state":"CO"}
View 端:Prototype 驅動 AJAX
<g:javascript library="prototype" />
<script>
function get(airportField) {
var baseUrl = "${createLink(controller: 'airport', action: 'getJson')}"
var url = baseUrl + "?iata=" + $F(airportField + "Iata")
new Ajax.Request(url, {
method: 'get',
asynchronous: true,
onSuccess: function(req) { update(req.responseText, airportField) }
})
}
function update(json, airportField) {
var airport = eval("(" + json + ")")
var output = $(airportField + "Text")
output.innerHTML = airport.iata + " - " + airport.name
var hiddenField = $(airportField + ".id")
airport.id == null ? hiddenField.value = -1 : hiddenField.value = airport.id
}
</script>
表單改造
<div id="departureAirportText">[Type an Airport IATA Code]</div>
<input type="hidden" name="departureAirport.id" value="-1" id="departureAirport.id"/>
<input type="text" name="departureAirportIata" id="departureAirportIata"/>
<input type="button" value="Find" onClick="get('departureAirport')"/>


<g:form action="save" method="post" onsubmit="return validate()">
function validate() {
if ($F("departureAirport.id") == -1) {
alert("Please supply a valid Departure Airport")
return false
}
if ($F("arrivalAirport.id") == -1) {
alert("Please supply a valid Arrival Airport")
return false
}
return true
}
belongsTo 決定,視業務語義靈活配置<select>/Radio/Checkbox;選項多用文字輸入 + AJAX 驗證掌握多對多建模與 AJAX 介面優化,下一篇將整合 Google Maps 視覺化航班路線,並介紹 Grails Service 層架構。