今天我們針對schema,分享一些進階的概念。
Abstract constraint可以幫助我們自己定義想要的constraint。例如:
abstract constraint must_contain_a() {
errmessage :=
'{__subject__} must contain at least one `a` or `A`.';
using (
contains(str_lower(__subject__), "a")
) ;
}
type User {
name: str {
constraint must_contain_a
}
}
這裡我們定義一個abstract constraint must_contain_a來確認其自身必須包含最少一個「"a"」或「"A"」字母。接著我們將must_contain_a施加於User object的name property。比較特別的是可以在errmessage中使用{__subject__}來顯示自身,有點像是Python的f-string功能。
此時,我們試著insert一個name property為「"John"」的User object:
select(insert User {name:="John"}) {name};
因為John內並沒有「"a"」或「"A"」字母,所以會報錯如下:
edgedb error: ConstraintViolationError: name must contain at least one `a` or `A`.
Detail: violated constraint 'default::must_contain_a' on property 'name' of object type 'default::User'
如果insert name property為「"May"」或「"MAY"」的User object,則皆會成功:
with names:= {"May", "MAY"}
for name in names
union (
select (insert User{name:=name}) {name}
);
{default::User {name: 'May'}, default::User {name: 'MAY'}}
abstract link可以幫助我們建立抽象化的link,使其可以作用在多個object type。
考慮schema如下:
abstract link link_with_note {
note: str;
}
type Person {
name: str;
multi friends: Person {
extending link_with_note;
};
}
可以看出abstract link link_with_note可以經由Person object extending後,作為Person object中的multi friends link;而abstract link link_with_note中的note則可以經由Person object以link property型式來存取。
此時我們insert兩個name property為「"John"」及「"Tom"」的Person object,並以John及Tom來代稱:
with names:= {"John", "Tom"}
for name in names
union (
select(insert Person {name:=name}) {name}
);
{default::Person {name: 'John'}, default::Person {name: 'Tom'}}
假設John是個常常記不清楚,朋友是在哪個階段認識的。此時他可以更新自己的multi friends link中的note link property來註記Tom是自己的高中同學:
with john:= (select Person filter .name="John"),
tom:= (select Person filter .name="Tom")
update john
set {
friends:= tom {@note:= "high school classmate"}
};
可以使用下面query確認註記成功:
select Person {**};
{
default::Person {
id: 7cc79298-53f1-11ef-926f-7364dedf390e,
name: 'John',
friends: {
default::Person {
id: 7cc794f0-53f1-11ef-926f-d7166079d714,
name: 'Tom',
@note: 'high school classmate'},
},
},
default::Person {
id: 7cc794f0-53f1-11ef-926f-d7166079d714,
name: 'Tom',
friends: {}},
}
不知道大家有沒有感受到,link property其實是個很有趣的功能呀?我自己是將link property想像為link的metadata,可以用來存取一些額外的資訊,並經由object type來存取。
最後提醒大家,官方文件中提到link property只能是single與optional。
本小節取材自Easy EdgeDB第十七章第四個練習題。如何能夠在保存data的情況下,修改object type的schema,將其中的property抽取出來為獨立的abstract type?
考慮schema如下:
type User {
email: str;
}
此時我們insert一個User object:
select (insert User {email:="John@example.com"}) {email};
{default::User {email: 'John@example.com'}}
此時我們可以將email property抽取為HasEmail,並改寫User object來extending HasEmail:
abstract type HasEmail {
email: str;
}
type User extending HasEmail;
接著執行migration:
did you create object type 'default::HasEmail'? [y,n,l,c,b,s,q,?]
> y
did you alter object type 'default::User'? [y,n,l,c,b,s,q,?]
> y
The following extra DDL statements will be applied:
ALTER TYPE default::User {
ALTER PROPERTY email {
DROP OWNED;
RESET TYPE;
};
};
(approved as part of an earlier prompt)
此時可以確認原先的User object的確仍然在資料庫內:
select User {*};
{default::User {id: d41b9f02-5406-11ef-a0e8-bb18dd29e982, email: 'John@example.com'}}
這個抽取技巧有點類似Python的Mixin或是Rust的trait。
本小節取材自官方文件。
backlink與multi link即是EdgeDB中的many-to-one及one-to-many關係。
舉例來說,我們將針對下面這個情況,分別使用backlink與multi link兩種方式來寫寫看:
User object可以擁有多件Shirt object。Shirt object只能被一個User object所擁有。backlink的想法是many-to-one,可以想成是many Shirt object to one Person object。
schema定義如下:
type Person {
required name: str
}
type Shirt {
required color: str;
owner: Person;
}
執行下面query,生成一個Person object及三個Shirt object:
insert Person {name:="John"};
with colors:= {"red", "green", "blue"}
for color in colors
union (
insert Shirt {
color:=color,
owner:= assert_single(Person)
}
);
此時,如果我們想由Person object下手,取得其所擁有的Shirt object時,可以寫為:
select Person {name, shirts:= .<owner[is Shirt]{color} };
{
default::Person {
name: 'John',
shirts: {
default::Shirt {color: 'red'},
default::Shirt {color: 'green'},
default::Shirt {color: 'blue'}
},
},
}
可以看出來shirts原本是沒有定義在schema內,而是我們使用backlink所取得的。
如果這樣的運算很常使用的話,也可以將其直接定義於schema內。例如:
type Person {
required name: str
shirts:= .<owner[is Shirt]
}
type Shirt {
required color: str;
owner: Person;
}
此時依然可以從Person object中取得其所擁有的Shirt object:
select Person {name, shirts: {color}};
{
default::Person {
name: 'John',
shirts: {
default::Shirt {color: 'red'},
default::Shirt {color: 'green'},
default::Shirt {color: 'blue'}
},
},
}
multi link的想法是one-to-many,可以想成是one Person object to many Shirt object。
schema定義如下:
type Person {
required name: str;
multi shirts: Shirt {
# ensures a one-to-many relationship
constraint exclusive;
}
}
type Shirt {
required color: str;
}
其中的constraint exclusive非常重要,可以確保一個Shirt object只能被一個Person object擁有。
執行下面query,生成一個Person object及三個Shirt object:
with colors:= {"red", "green", "blue"}
for color in colors
union (
insert Shirt {
color:=color,
}
);
insert Person {name:="John", shirts:= Shirt};
由Person object中取得其所擁有的Shirt object:
select Person {name, shirts: {color}};
{
default::Person {
name: 'John',
shirts: {
default::Shirt {color: 'red'},
default::Shirt {color: 'green'},
default::Shirt {color: 'blue'}
},
},
}
官方文件中建議當有下列兩種情況的時候使用multi link,否則建議使用single link搭配backlink:
annotation可以幫助我們提供一些註記給object type。EdgeDB預設有title、description及deprecated三種。
考慮schema如下:
type User {
annotation deprecated := "BREAKING CHANGE! As of version x.y.z, the `User` object will be renamed to `USER`."
}
我們給予User object一個deprecated annotation。
此時可以利用introspect可以檢視User object的內部資訊:
select (introspect User) { annotations: {name, @value}};
{
schema::ObjectType {
annotations: {
schema::Annotation {
name: 'std::deprecated',
@value: 'BREAKING CHANGE! As of version x.y.z, the `User` object will be renamed to `USER`.',
},
},
},
}
除了預設的三種annotation外,我們也可以自己定義。例如,這裡我們自己定義了一個hello annotation:
abstract annotation hello;
type User {
annotation hello := "hello"
}
此時一樣可以利用introspect來檢視User object:
select (introspect User) { annotations: {name, @value}};
{
schema::ObjectType {
annotations: {
schema::Annotation {
name: 'default::hello',
@value: 'hello'
}
}
}
}
可以確認User object確實註記有hello annotation。
Trigger就像一個callback,可以在object type進行某一種操作後,接著執行另一個操作(但兩者會在同一個transaction內)。
rewrite則是攔截insert或update等mutation指令,並依據預先設定的expression來改寫property或link後,再傳至database。
官方文件中提到這個例子:
type User {
required name: str;
trigger log_insert after insert for each do (
insert Log {
action := 'insert',
target_name := __new__.name
}
);
}
在每次insert一個User object同時,也會insert一個Log object。
此外,文件中也提到trigger可能會引發callback地獄,需要小心使用。
官方文件中提到這個例子:
type Post {
required title: str;
required body: str;
modified: datetime {
rewrite insert, update using (datetime_of_statement())
}
}
在每一次進行insert或update時,EdgeDB會自動攔截並改寫query,將執行datetime_of_statement()後的值指定給modified property,再傳給database。
EdgeDB在下面三個地方,會自動幫大家進行index:
object自動產生的id(可以想成primary key)。link(可以想成foreign key)。constraint exclusive的property。此外,也可以直接使用Postgres提供的index。