iT邦幫忙

2024 iThome 鐵人賽

DAY 26
0
Software Development

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

[Day26] - 進階EdgeQL語法介紹

  • 分享至 

  • xImage
  •  

今天我們針對EdgeQL語法,分享一些進階的概念。

filter vs in的細微差異

考慮schema如下:

type User {
    required name: str {
        constraint exclusive
    };
}

type Article {
    required title: str;
    author: User;
}

建立一個Article object與一個User object(簡稱為John):

insert Article {
    title:= "first article",
    author:= (insert User {name:= "John"})
};

此時如果想再建立一個Article object,其author link也是John的話,可以這麼寫:

insert Article {
    title:= "second article",
    author:= (select User filter .name="John")
};

由於Username propertyconstraint exclusive,所以可以保證(select User filter .name="John")最多只會返回一個User object,確保authorsingle link

但如果是執行下面這個query:

insert Article {
    title:= "second article",
    author:= (select User filter .name in {"John"})
};

EdgeDB則會報錯:

error: QueryError: possibly more than one element returned by an expression for a link 'author' declared as 'single'

這是因為in {}是一種set operation,需要加上assert_single()才可以順利執行:

insert Article {
    title:= "second article",
    author:= assert_single(
            (select User filter .name in {"John"})
    )
};

Type intersection

Restrict type

考慮schema如下:

abstract type Integer;
type PositiveInteger extending Integer;
type NegativeInteger extending Integer;
type Zero extending Integer;

insert兩個PositiveInteger object、一個NegativeInteger object及一個Zero object

insert PositiveInteger;
insert PositiveInteger;
insert NegativeInteger;
insert Zero;

此時如果執行:

select Integer;
{
  default::PositiveInteger {id: 70699990-54c0-11ef-9775-df3dcebe0d15},
  default::PositiveInteger {id: 712a6d50-54c0-11ef-912e-ab352977af3c},
  default::Zero {id: 7db809b0-54c0-11ef-912e-8fa4e8f6afa6},
  default::NegativeInteger {id: 7b5512b2-54c0-11ef-912e-9fa4d590cb9d},
}

會選到剛剛insert的四個object

此時如果我們仍然想由Integer object出發,來選取所有的PositiveInteger object的話,可以使用[is Type]的語法,像是`:

select Integer[Is PositiveInteger];
{
  default::PositiveInteger {id: 70699990-54c0-11ef-9775-df3dcebe0d15},
  default::PositiveInteger {id: 712a6d50-54c0-11ef-912e-ab352977af3c},
}

這樣就可以選取到兩個PositiveInteger object

Splat

考慮一個模擬候選人與支持者關係的schema如下:

abstract type Person {
    required name: str { 
        constraint exclusive 
    };
}

type Candidate extending Person {
    party: str;
    multi supporters := .<endorsed_candidate[is Supporter];
}

type Supporter extending Person {
    endorsed_candidate: Candidate
}

insert兩個Candidate object及四個Supporter object

insert Candidate {
    name:= "John",
    party:= "Party A",
};


insert Candidate {
    name:= "Cathy",
    party:= "Party B",
};


for name in {"Mark", "May"}
  union (
    insert Supporter {
        name := name,
        endorsed_candidate := (
            select Candidate filter .name = "John"
        )
     }
);


for name in {"Jeff", "Lisa"}
  union (
    insert Supporter {
        name := name,
        endorsed_candidate := (
            select Candidate filter .name = "Cathy"
        )
     }
);

假設我們想從Person出發,選擇所有的Person object,但卻只想展示Candidate objectproperty時,可以寫成:

select Person {name, [is Candidate].*};
{
  default::Candidate {
    name: 'John', 
    id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9, 
    party: 'Party A'
  },
  default::Candidate {
    name: 'Cathy', 
    id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe, 
    party: 'Party B'
  },
  default::Supporter {
    name: 'Mark', 
    id: 43591bfe-54c7-11ef-ade1-ef6840a01210, 
    party: {}
  },
  default::Supporter {
    name: 'May', 
    id: 4359270c-54c7-11ef-ade1-c3491717aa17, 
    party: {}
  },
  default::Supporter {
    name: 'Jeff', 
    id: 43aa506e-54c7-11ef-8e3f-dba6207b9cf4, 
    party: {}
  },
  default::Supporter {
    name: 'Lisa', 
    id: 43aa5352-54c7-11ef-8e3f-174d38b12195, 
    party: {}
  },
}

這樣的好處是選擇出來的shape是一致的。由於我們是針對Candidate objectproperty來選擇(只使用{*}),所以:

  • endorsed_candidate link沒有被選擇到。
  • Supporter objectparty property為空EdgeDBSet

如果想包含endorsed_candidate link的話,可以寫成:

select Person {name, [is Candidate].**};
{
  default::Candidate {
    name: 'John',
    id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9,
    party: 'Party A',
    supporters: {
      default::Supporter {
        name: 'Mark', 
        id: 43591bfe-54c7-11ef-ade1-ef6840a01210
      },
      default::Supporter {
        name: 'May', 
        id: 4359270c-54c7-11ef-ade1-c3491717aa17
      },
    },
  },
  default::Candidate {
    name: 'Cathy',
    id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe,
    party: 'Party B',
    supporters: {
      default::Supporter {
        name: 'Jeff', 
        id: 43aa506e-54c7-11ef-8e3f-dba6207b9cf4
      },
      default::Supporter {
        name: 'Lisa', 
        id: 43aa5352-54c7-11ef-8e3f-174d38b12195
      },
    },
  },
  default::Supporter {
    name: 'Mark', 
    id: 43591bfe-54c7-11ef-ade1-ef6840a01210, 
    party: {}, 
    supporters: {}
  },
  default::Supporter {
    name: 'May', 
    id: 4359270c-54c7-11ef-ade1-c3491717aa17, 
    party: {}, 
    supporters: {}
  },
  default::Supporter {
    name: 'Jeff', 
    id: 43aa506e-54c7-11ef-8e3f-dba6207b9cf4, 
    party: {}, 
    supporters: {}
  },
  default::Supporter {
    name: 'Lisa', 
    id: 43aa5352-54c7-11ef-8e3f-174d38b12195, 
    party: {}, 
    supporters: {}
  },
}

最後,如果您想從Candidate出發,卻只想選擇Person中的property的話,可以寫成:

select Candidate {Person.*};
{
  default::Candidate {
    id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9, 
    name: 'John'
  },
  default::Candidate {
    id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe, 
    name: 'Cathy'
  },
}

如果想包含Person中的propertylink的話,可以寫成:

select Candidate {Person.**};
{
  default::Candidate {
    id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9, 
    name: 'John'
  },
  default::Candidate {
    id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe, 
    name: 'Cathy'},
}

不過因為我們的Person中沒有link,所以使用{*}{**}的結果是一樣的。

進階filter概念

本章延續前面splat的schema及database。

Query1

在研究type intersection的過程中,發現原來我們可以在對link使用shape時進行nested filter:

select Candidate {
    name,
    supporters: {name} filter .name="Mark"
} filter .name="John";
{
  default::Candidate {
    name: 'John', 
    supporters: {
      default::Supporter {name: 'Mark'}
    }
  }
}

query1先過濾了Candidate object.name="John",接著再過濾了supporters link.name="Mark"

Query2

query2先過濾了Candidate object.name="John",接著再過濾了Candidate object.supporters.name="Mark"

select Candidate {
    name,
    supporters: {name}
} filter .name="John" and .supporters.name="Mark";
{
  default::Candidate {
    name: 'John',
    supporters: {
      default::Supporter {name: 'Mark'}, 
      default::Supporter {name: 'May'}
    },
  },
}

可以看出query2與query1的結果並不相同。

可能您會覺得奇怪為什麼query2中的supporters中會有兩個Supporter object呢?這是因為我們是針對Candidatename propertysupporters link來選擇,不管過濾的條件為何,Candidate objectshape已經決定。所以這相當於是在過濾了.name="John"後,再次確認.supporters.name="Mark"是否符合。如果都符合的話,則選取此Candidate object並展現其指定的shape

可能您還是非常困惑,我相信這是因為對EdgeDB的element-wise特性還不夠熟悉所致。下面這個query應該能夠幫助您:

select Candidate.supporters.name = "Mark";
{true, false, false, false}

這個query的結果是將Candidate.supporters.name這個EdgeDBSet中的每個元素與「"Mark"」相比,如果相等的話返回true,否則返回false。

再回到query2,我們需要以and為分界來思考:

  • 首先針對全部Candidate object,選出.name="John"為true的Candidate object,結果應該只有John一個。
  • 接著再針對John(不是全部Candidate object,因為EdgeDB只需要針對and前半部返回trueCandidate object來篩選就好),選出符合.supporters.name="Mark"Candidate object,結果應該還是只有John一個。這邊需留意此處其實進行了兩次比較,分別是「"Mark" = "Mark"」與「"May" = "Mark"」,由於只有「"Mark" = "Mark"」會返回true,所以返回John。假如name property不是constraint exclusive的話,而Johnsupporters有兩個Mark時,則此處會比較兩次「"Mark" = "Mark"」,並返回兩個John

Conflicts

考慮schema如下:

type Customer {
    name: str {
        constraint exclusive
    };
    cost: float64
}

Customer typename propertycost property,分別記錄客人的名字及總消費金額。

當有客人進行消費時,我們想進行的query為:

  • 當該客人未曾在店家消費時,新增一個Customer object,其cost property設為此次消費金額。
  • 當該客人曾經在店家消費時,更新該Customer object,使其cost property為之前消費金額再加上此次消費金額。

此時就可以運用到conflicts語法來處理。

首先我們假設Johnname property為「"John"」的Customer object,而Cathyname property為「"Cathy"」的Customer object

執行下面query,代表John第一次消費「5」元:

insert Customer {
    name:= "John",
    cost:= 5
};

接著我們可以將John再次消費「10」元,與Cathy第一次消費「5」元的情形,合併寫為下面query:

with customers:= {(name:="John", cost:=10), (name:="Cathy", cost:=20)}
for customer in customers
union (
    insert Customer {
        name:= customer.name,
        cost:= customer.cost
    }
    unless conflict on .name
    else (
        with c:= (select Customer filter .name=customer.name)
        update c
        set {
            cost := .cost + customer.cost
        }
    )
);

上面query代表我們先試著insert一個User object,但是當其name property違反constraint exclusive時,代表該User object已經存在於資料庫。此時,我們可以執行else區塊內的update query。

此時,我們觀察所有User object

select Customer{name, cost};
{
  default::Customer {name: 'Cathy', cost: 20}, 
  default::Customer {name: 'John', cost: 15}
}

可以確認:

  • John的兩次消費總額為5+10=15元,正確更新。
  • Cathy的首次消費記錄為5元。

Order by .. then

本小節參考自官方文件

考慮schema如下:

type Movie {
    title: str;
    release_year: int64
}

我們想選擇所有Movie object並根據下面兩點進行不同的排序:

  • 當輸入為字串「"title"」時,選擇title property作為排序對象。
  • 當輸入為字串「"release_year"」時,選擇release_year property作為排序對象。

這個query並不容易撰寫,原因是因為兩個property是不同型別的。為此官方文件給出建議方式是利用order by + then語法來完成。

我們先insert兩個Movie object

insert Movie {title:= "Tom Wick", release_year:=2008};
insert Movie {title:= "Steel Man", release_year:=2014};

接著輸入官方建議的query:

select Movie {*}
  order by
    (.title if <str>$order_by = 'title'
      else <str>{})
  then
    (.release_year if <str>$order_by = 'release_year'
      else <int64>{});

此時當我們輸入:

  • 字串「"title"」,即會以title property為排序對象。
  • 字串「"release_year"」,即會以release_year property為排序對象。
  • 其它非「"title"」或「"release_year"」的任何字串,則會以<str>{}為排序對象。

這是個有趣的技巧,但是實務上我也常常忘記有這種語法。

事實上,如果兩個property皆為同一型別的話,例如:

type Movie {
    title: str;
    release_year: str
}

我們可以使用多個if..else來簡化query,例如:

with order_by:= <str>$order_by
select Movie {*}
order by
(
    .title if order_by = 'title' else
    .release_year if order_by = 'release_year' else
    <str>{}
);

在同型別的情況下,我會傾向使用多個if..else語法。


上一篇
[Day25] - 進階Schema介紹
下一篇
[Day27] - 使用SVCS、FastHTML搭配EdgeDB建立Python todo app(1)
系列文
一起看無間道學EdgeDB30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言