韓琛千鈞一髮之際收到建明簡訊,緊急打給迪路與傻強,將與泰國佬交易的可卡因丟進海裡。黃sir見行動失敗,只得暫時將韓琛及其手下帶回警察局。黃sir確認證據不足以起訴韓琛後,帶隊來到韓琛用餐的房間。黃sir藉機嘲諷韓琛,雖然這次無法逮捕他,但已令他損受幾千萬。韓琛聽後,瞬間翻臉將桌上食物往黃sir座位掃去。兩人言談間針鋒相對,互相猜測著對方安排在己方的臥底是誰。最後,韓琛囂張地帶著手下們大步離去。
Beverage
全劇至此已是第三次喝飲料了,不禁讓我們想要建立一個Beverage
來記錄所有人喝過的飲料。
Beverage
有一個property
和四個link
:
name property
為必填的飲料名。produced_by link
為生產廠家。consumed_by link
為被誰所喝。when link
為何時被喝。where link
為在哪裡被喝。type Beverage {
required name: str;
produced_by: Store;
consumed_by: Character;
`when`: FuzzyTime;
where: Place;
}
did you create object type 'default::Beverage'? [y,n,l,c,b,s,q,?]
> y
insert
店家龍鼓灘在開始進行Beverage
相關操作前,我們先insert
一個龍鼓灘Store object
,代表一家名為龍鼓灘茶餐廳。
insert Store {name:="龍鼓灘"};
如果您還記得的話,上一個場景我們也有名為一個龍鼓灘的Landmark object
。由於Store
及Landmark
都是extending
自Place
,讓我們回顧一下Place
的schema:
abstract type Place {
required name: str {
delegated constraint exclusive;
};
}
由於Place
的name property
使用了delegated constraint exclusive
,所以Store
及Landmark
各自都可以建立自己的龍鼓灘。
但是如果想再insert
一個龍鼓灘Store object
的話,則會報錯如下:
edgedb error: ConstraintViolationError: name violates exclusivity constraint
Detail: property 'name' of object type 'default::Store' violates exclusivity
constraint
insert
Beverage
有喝飲料的角色太多了,讓我們insert
三種飲料代表就好。
第一種是熱奶茶,為建明於下午茶時間喝的(註1),為龍鼓灘茶餐廳出品。
insert Beverage {
name:= "熱奶茶",
produced_by:= assert_single((select Store filter .name="龍鼓灘")),
consumed_by:= lau,
`when`:= (insert FuzzyTime {fuzzy_hour:=15, fuzzy_minute:=15}), #三點三
where:= police_station,
};
第二種是保特瓶裝綠茶(註2),為建明在緝毒過程中請國平拿給他的。我們假設此時為20:15。
insert Beverage {
name:= "綠茶",
consumed_by:= lau,
`when`:= (insert FuzzyTime {fuzzy_hour:=20, fuzzy_minute:=15}),
where:= assert_single((select Location filter .name="大廈三樓")),
};
第三種是韓琛在警局被拘留時所喝的飲料,我們假設為龍鼓灘茶餐廳出品的凍檸茶並假設此時為23:15。
insert Beverage {
name:= "凍檸茶",
produced_by:= assert_single((select Store filter .name="龍鼓灘")),
consumed_by:= hon,
`when`:= (insert FuzzyTime {fuzzy_hour:=23, fuzzy_minute:=15}),
where:= police_station,
};
backlink
假如我們想知道建明喝了哪些飲料,可以這麼做:
select Beverage {name} filter .consumed_by=lau;
{default::Beverage {name: '熱奶茶'}, default::Beverage {name: '綠茶'}}
但是如果除了所喝的飲料,您又同時想顯示出建明的其它資訊,這時候可以使用backlink
來做:
select lau {name, nickname, beverages:= .<consumed_by[is Beverage] {name}};
{
default::GangsterSpy {
name: '劉建明',
nickname: '劉仔',
beverages: {
default::Beverage {name: '熱奶茶'},
default::Beverage {name: '綠茶'}},
},
}
別忘了在backlink
中也可以使用shape
,像是這邊的{name}
,來選取想要的property
或link
。
也就是說您可以寫出像是下面這段有趣的query:
select lau {
name,
nickname,
beverages:= .<consumed_by[is Beverage] {
name,
where : {name}
}
};
{
default::GangsterSpy {
name: '劉建明',
nickname: '劉仔',
beverages: {
default::Beverage
{
name: '熱奶茶',
where: default::Landmark {name: '警察局'}
},
default::Beverage
{
name: '綠茶',
where: default::Location {name: '大廈三樓'}
},
},
},
}
這相當於同時列出:
name property
。nickname property
。至於何時適合使用backlink
呢?當您想取得被aaa
所bbb
的ccc
時,例如被建明(aaa
)所喝掉(bbb
)的飲料(ccc
)時。此時對aaa
使用select
,ccc
通常可以使用backlink
的語法一起出現在select
的{}
內。
TeamTreatNumber
及CIBTeamTreat
假如CIB部門有一個傳統,當全組人需要一起留下加班處理特殊案件時,部門內除長官外的每一個同事都可以在抽獎箱裡抽出一顆球(抽完後球會放回箱內)。箱內總共有1~10十顆球,上面貼有號碼,只要有人抽到的號碼是9或10的話,就會由部門長官買單請吃一頓下午茶。由於每次操作這個活動都需要花費不少時間,所以有組員提議請我們使用EdgeDB來簡化流程。身為無間道和EdgeDB的雙重愛好者,我們當然是義不容辭啦!
首先建立TeamTreatNumber
,其是由extending
sequence
而來,作為每次活動的計數器。
scalar type TeamTreatNumber extending sequence;
接著建立CIBTeamTreat
如下:
type CIBTeamTreat {
required team_treat_number: TeamTreatNumber {
constraint exclusive;
default := sequence_next(introspect TeamTreatNumber);
}
multi colleagues: Police {
default:= (select Police filter .dept="刑事情報科(CIB)");
readonly := true;
point: int64 {
default:= <int64>math::ceil(random()*10)
}
};
team_treat:= max(.colleagues@point) >= 9
}
CIBTeamTreat
有兩個property
和一個link
,我們逐個來看。
team_treat_number
team_treat_number
為一property
,其使用TeamTreatNumber
為記數器,和Scene
的scene_number
一樣都是自動產生不重覆的編號。
colleagues
colleagues
是一個Police
的multi link
,其預設選擇CIB部門的所有警察,這符合我們的需求,因為長官不須參加抽獎(記得建明是GangsterSpy
嗎?)。接著我們加上readonly
為true
的限制,防止大家抽完獎之後耍賴偷改。
最後我們新增一個int64
的point
,這是一個link property
,顧名思義其為一個colleagues
link
的property
。這個link property
並不能由Police
來存取(雖然它現在正在Police
的{}
中),它只能夠在我們針對CIBTeamTreat
query時存取。我們使用內建的math::ceil()
及random()
來給予CIB部門所有警察一個1~10的預設值,模擬抽獎過程(每個警察都會抽一次,且不會全部都是同一個數字)。
team_treat
team_treat
為一computed property
(留意這邊使用的是:=
),其會返回一個bool
值。存取link property
需要使用特殊的@
符號,所以.colleagues@point
相當於動態取得該CIBTeamTreat
內colleagues
multi link
內的所有point
。我們使用內建的max()
來看看這些point
內的最大值是否會大於或等於9。
did you create scalar type 'default::TeamTreatNumber'? [y,n,l,c,b,s,q,?]
> y
did you create object type 'default::CIBTeamTreat'? [y,n,l,c,b,s,q,?]
> y
insert
CIBTeamTreat
讓我們來試試這個抽獎系統吧。下面的query每次都會insert
一個CIBTeamTreat
,並返回其team_treat
之值。如果是true
的話,代表建明需要請客。
select(insert CIBTeamTreat).team_treat;
您可能會想將抽獎系統寫為function
,像是:
# ❌
function draw() -> CIBTeamTreat
using (select(insert CIBTeamTreat));
但是如果執行edgedb migration create
會報錯如下:
error: data-modifying statements are not allowed in function bodies
原來function
內是不能對資料庫進行變動的,包括insert
、update
及delete
。
有時候因為太久沒長官請客,大家會懷疑系統是不是出錯了,那麼可以由下面這個query來列出所有人抽到的數字。
select(insert CIBTeamTreat)
{team_treat_number, team_treat, points:= .colleagues@point};
{default::CIBTeamTreat
{team_treat_number: 2, team_treat: false, points: {8, 1, 1, 4, 6, 2}}}
最後,同事們間可能互相調侃誰最帶賽,此時可以用下面的query列出每個人的名字及其抽到的數字。
select(insert CIBTeamTreat)
{team_treat_number, team_treat, colleagues: {name, @point}};
{
default::CIBTeamTreat {
team_treat_number: 3,
team_treat: true,
colleagues: {
default::Police {name: '林國平', @point: 1},
default::Police {name: '大象', @point: 10},
default::Police {name: '孖八', @point: 2},
default::Police {name: 'police_11', @point: 7},
default::Police {name: 'police_12', @point: 1},
default::Police {name: 'police_13', @point: 7},
},
},
}
例如像第三次抽獎,國平和名為police_12
的同事都只抽到1,不過還好大象抽到10,最後大家還是有免費下午茶吃。
現在開始,每次的抽獎記錄都會是一個CIBTeamTreat object
。這些記錄累積起來可以有許多應用,例如每年年末,大家可以找出誰是最常幫大家贏得下午茶的人(最常拿9或10分),一起請他吃頓飯。link property
是不是一個很酷的功能呀!
link property
有一些語法如果不常使用,的確容易忘記。為此EdgeDB貼心準備了cheatsheet供大家參考。
update
hon
話說這個場景好像充滿著飲料,讓我們回來繼續關心琛哥,update
他離開警察局前對黃sir說的經典台詞到classic_lines
。
update hon
set {
classic_lines := .classic_lines ++ ["你見過有人去殯儀館和屍體握手嗎?"],
};
insert
ChenLauContact
insert ChenLauContact {
how:= "面對面",
detail:= "毒品被韓琛手下迪路與傻強銷毀,永仁隨韓琛一起被帶回警察局",
`when`:= year_2002,
where:= police_station,
};
insert
此場景的Scene
insert Scene {
title:= "互猜底牌",
detail:= "韓琛千鈞一髮之際收到建明簡訊,緊急打給迪路與傻強,將與" ++
"泰國佬交易的可卡因丟進海裡。黃sir見行動失敗,只得暫時" ++
"將韓琛及其手下帶回警察局。回警察局後,黃sir確認證據不" ++
"足以起訴韓琛後,帶隊來到韓琛用餐的房間。黃sir藉機嘲諷" ++
"韓琛,雖然這次無法逮捕他,但以令他損受幾千萬。韓琛聽" ++
"後,瞬間翻臉將桌上食物往黃sir座位掃去。兩人言談間針鋒" ++
"相對,互相猜測著對方安排在己方的臥底是誰。最後,韓琛囂" ++
"張地帶著手下們大步離去。",
who:= (select Gangster filter .nickname in {"迪路", "傻強"})
union {wong, chen, hon, lau},
`when`:= year_2002,
where:= police_station,
remarks:= "1.假設建明喝綠茶時間為20:15。\n2.假設韓琛於23:15喝凍檸茶。"
};
根據訪談,曾志偉橫掃桌上飯菜的經典戲碼,黃秋生事先並不知情。但他根據自己的經驗判斷,曾志偉必定會這樣做,目的是使自己大吃一驚。於是他在演戲時,已經做好往後退的準備,因為不想穿著湯湯水水的衣服繼續演戲。
註1:粵語中形容分鐘時,習慣將六十分鐘分作十二份,每個數字對應五分鐘,所以下午三點三即代表15:15。
註2:由於這似乎是華仔代言的牌子,所以我們就不給定produced_by
,來幫忙打廣告了。有趣的是,劇中還有一幕,建明打電話請同事跟蹤黃sir時,他又喝了一次同牌子的綠茶。此外,在無間道Ⅲ也會多次看到這個牌子,像是開頭建明與國平聊天時,永仁與黃sir見面的便利商店背景或是警察局的飲料販賣機等。