iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0
Software Development

一起看無間道學EdgeDB系列 第 20

[Day20] - 七幕:互猜底牌

  • 分享至 

  • xImage
  •  

Full schema preview

本日所有schema搶先看

劇情提要

韓琛千鈞一髮之際收到建明簡訊,緊急打給迪路與傻強,將與泰國佬交易的可卡因丟進海裡。黃sir見行動失敗,只得暫時將韓琛及其手下帶回警察局。黃sir確認證據不足以起訴韓琛後,帶隊來到韓琛用餐的房間。黃sir藉機嘲諷韓琛,雖然這次無法逮捕他,但已令他損受幾千萬。韓琛聽後,瞬間翻臉將桌上食物往黃sir座位掃去。兩人言談間針鋒相對,互相猜測著對方安排在己方的臥底是誰。最後,韓琛囂張地帶著手下們大步離去。

scene07

此劇照引用自IMDb-無間道

EdgeQL query

建立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;
}

執行1st migration

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。由於StoreLandmark都是extendingPlace,讓我們回顧一下Place的schema:

abstract type Place {
    required name: str {
        delegated constraint exclusive;
    };
}

由於Placename property使用了delegated constraint exclusive,所以StoreLandmark各自都可以建立自己的龍鼓灘。

但是如果想再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},來選取想要的propertylink

也就是說您可以寫出像是下面這段有趣的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呢?當您想取得被aaabbbccc時,例如被建明(aaa)所喝掉(bbb)的飲料(ccc)時。此時對aaa使用selectccc通常可以使用backlink的語法一起出現在select{}內。

建立TeamTreatNumberCIBTeamTreat

假如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為記數器,和Scenescene_number一樣都是自動產生不重覆的編號。

colleagues

colleagues是一個Policemulti link,其預設選擇CIB部門的所有警察,這符合我們的需求,因為長官不須參加抽獎(記得建明是GangsterSpy嗎?)。接著我們加上readonlytrue的限制,防止大家抽完獎之後耍賴偷改。

最後我們新增一個int64point,這是一個link property,顧名思義其為一個colleagues linkproperty。這個link property並不能由Police來存取(雖然它現在正在Police{}中),它只能夠在我們針對CIBTeamTreatquery時存取。我們使用內建的math::ceil()random()來給予CIB部門所有警察一個1~10的預設值,模擬抽獎過程(每個警察都會抽一次,且不會全部都是同一個數字)。

team_treat

team_treat為一computed property(留意這邊使用的是:=),其會返回一個bool值。存取link property需要使用特殊的@符號,所以.colleagues@point相當於動態取得該CIBTeamTreatcolleagues multi link內的所有point。我們使用內建的max()來看看這些point內的最大值是否會大於或等於9。

執行end migration

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內是不能對資料庫進行變動的,包括insertupdatedelete

有時候因為太久沒長官請客,大家會懷疑系統是不是出錯了,那麼可以由下面這個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見面的便利商店背景或是警察局的飲料販賣機等。

參考資料

無間EdgeDB七幕:互猜底牌


上一篇
[Day19] - 六幕:有內鬼終止交易
下一篇
[Day21] - 八幕:誰是內鬼
系列文
一起看無間道學EdgeDB30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言