iT邦幫忙

2024 iThome 鐵人賽

DAY 25
0
Software Development

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

[Day25] - 進階Schema介紹

  • 分享至 

  • xImage
  •  

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

Abstract constraint

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 objectname 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

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 objectlink property型式來存取。

此時我們insert兩個name property為「"John"」及「"Tom"」的Person object,並以JohnTom來代稱:

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想像為linkmetadata,可以用來存取一些額外的資訊,並經由object type來存取。

最後提醒大家,官方文件中提到link property只能是singleoptional

Abstract type應用範例

本小節取材自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 objectextending 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 vs multi link

本小節取材自官方文件

backlinkmulti link即是EdgeDB中的many-to-oneone-to-many關係。

舉例來說,我們將針對下面這個情況,分別使用backlinkmulti link兩種方式來寫寫看:

  • User object可以擁有多件Shirt object
  • 一個Shirt object只能被一個User object所擁有。

Backlink寫法

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寫法

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

annotation可以幫助我們提供一些註記給object type。EdgeDB預設有titledescriptiondeprecated三種。

考慮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 vs rewrite

Triggerrewrite是兩個互補的功能。

Trigger就像一個callback,可以在object type進行某一種操作後,接著執行另一個操作(但兩者會在同一個transaction內)。

rewrite則是攔截insertupdatemutation指令,並依據預先設定的expression來改寫propertylink後,再傳至database。

Trigger

官方文件中提到這個例子:

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地獄,需要小心使用。

Rewrite

官方文件中提到這個例子:

type Post {
    required title: str;
    required body: str;
    modified: datetime {
        rewrite insert, update using (datetime_of_statement())
    }
}

在每一次進行insertupdate時,EdgeDB會自動攔截並改寫query,將執行datetime_of_statement()後的值指定給modified property,再傳給database。

index

EdgeDB在下面三個地方,會自動幫大家進行index

  • 每個object自動產生的id(可以想成primary key)。
  • 每個link(可以想成foreign key)。
  • 每個constraint exclusiveproperty

此外,也可以直接使用Postgres提供的index


上一篇
[Day24] - EdgeDBSet概念加強
下一篇
[Day26] - 進階EdgeQL語法介紹
系列文
一起看無間道學EdgeDB30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言