almost 2 years ago

系列文一
系列文二
系列文三
良好的分table與建立table間的relationship對於RDBMS的性能有大大的影響(觀<<高性能MySQL>>有感),但這一篇也不是主講這個(也超出我的能力範圍...),這篇只是介紹如何透過Sequelize定義relationship與後續的CRUD操作。
附上簡易的UML,基本上就是 每個主人有一隻本命神奇寶貝與數隻神奇寶貝,而每隻神奇寶貝只能有一位主人;神奇寶貝可以有多個技能,每個技能也可以被不同的神奇寶貝學;情境對應 Owner/Pokemon/Skill,關係分別為1:1/1:N/N:M
在Sequelize中可以在table宣告時就定義Foreign Key或是methods來創建關聯,見源碼

1:1

Sequelize提供了hasOnebelongsTo兩種定義1:1的方法,其中差別在於foreignKey建立在哪張table中與methods擴充在哪個物件上,定義方式如A.hasOne(B) or A.belongsTo(B),其中A表示source,B表示Target
hasOne會將ForeingKey加在Target(B)上,相反的belongsTo則夾到Source(A)上
首先來創建飼主與本命神奇寶貝的1:1關聯

//Owner會多一欄 RootPokeMonId

Owner.belongsTo(Pokemon, {as:'RootPokeMon', foreignKeyContraints:false});

值得注意的是這裡
a. foreignKeyContraints要設為false,因為預設Foriegn Key不允許為Null,這會帶來許多麻煩的影響,例如說 我想要先創建飼主但不想馬上配給她本命神奇寶貝,這樣就不允許!(玩遊戲也是要到大木博士家才能領神奇寶貝啊),所以建議是先關掉,同樣的道理可以參考這篇Can a foreign key be NULL and/or duplicate?,裡頭的例子和我舉得差不多。
b. 命名規則,Sequelize會自動創建getsetmethods,那命名方式就是再加上table name(首字大寫),如getPokemon(),但因為我有設定as,所以就變成getRootPokemon,同樣的ForiegnKey ID name也可以自取,預設為tablename+Id

設定Foreign key
將對應執行的SQL log出來比對就蠻好理解的

Executing (default): INSERT INTO owner (name,id) VALUES ('小智',DEFAULT);
Executing (default): INSERT INTO pokemon (name,level,id) VALUES ('皮卡丘',5,DEFAULT);
//setRootPokemon()
Executing (default): SELECT name, level, id FROM pokemon AS pokemon WHERE pokemon.id = 1;
Executing (default): UPDATE owner SET RootPokeMonId=1 WHERE id = 1
//getRootPokemon()
Executing (default): SELECT name, level, id FROM pokemon AS pokemon WHERE pokemon.id = 1;

sequelize.sync({force:true}).then(()=>{
    //創建飼主與神奇寶貝

    return Promise.all([Owner.create({name:'小智'}),Pokemon.create({name:'皮卡丘'})]);
}).then((value)=>{
    //綁定飼主與神奇寶貝

    return value[0].setRootPokeMon(value[1]);
}).then((owner)=>{
    //取得飼主的本命神奇寶貝

    console.log(owner);
    return owner.getRootPokeMon();
}).then((val)=>{
    //log神奇寶貝

    console.log(val);
});
1:N

接著定義飼主與眾多神奇寶貝們,這裡定義了一隻神奇寶貝屬於一位飼主一個飼主有多隻神奇寶貝,一樣要注意get和set與foreignKey的定義,這一段多增加constraint:false是因為 Sequelize不允許Cyclic出現,也就是Pokemon和Owner彼此雙向關聯,如果沒有設定則Error產生如下Dependency chain: pokemon -> owner => pokemon

Pokemon.belongsTo(Owner,{foreignKey: 'owner_id', foreignKeyContraints:false, constraints: false});
Owner.hasMany(Pokemon,{foreignKey: 'owner_id', foreignKeyContraints:false, constraints: false});

更新綁定方式,這裡我在Pokemon Model增加了

defaultScope:{
        where:{
            level:{
                $gt:6
            }
        }
    }

可以看到這會影響到之後所有的get* methods,可以透過{scope:false}取消

.then((value)=>{
    //綁定飼主與神奇寶貝
    return Promise.all([value[0].setRootPokeMon(value[2]),
                        value[2].setOwner(value[0]),
                        value[3].setOwner(value[0]),
                        value[1].setRootPokeMon(value[4]),
                        value[4].setOwner(value[1]),
                        value[5].setOwner(value[1]),
                        value[6].setOwner(value[1]),
                        value[7].setOwner(value[1]),
                    ]);
}).then((val)=>{
    console.log(val);
    //取得訓練家小智旗下所有高於六等的神奇寶貝(見scope)
    return val[0].getPokemons()
N:M

這裡需要兩個Model都使用belongsToMany,在belongsToMany中定義throughthrough中定義 relation table,Sequelize會自動創建這個relation table也可以透過自己定義,那relation table中主要是放ModelA和ModelB兩者的Foriegn Key。

Pokemon.belongsToMany(Skill,{through: 'PokemonSkills', foreignKeyContraints:false, constraints: false});
Skill.belongsToMany(Pokemon,{through: 'PokemonSkills', foreignKeyContraints:false, constraints: false});

PokemonSkills (pokemonId,skillId,createdAt,updatedAt) //自動插入創建時間

belongsToMany會讓ModelA增加 addModelB、addModelBs、setModelBs、getModelBs,同理套用到ModelB上

return Promise.all([Owner.create({name:'小智'}),
                        Owner.create({name:'小茂'}),
                        Pokemon.create({name:'皮卡丘',level:'10'}),
                        Pokemon.create({name:'卡比獸',level:'17'}),
                        Pokemon.create({name:'水箭龜',level:'8'}),
                        Pokemon.create({name:'噴火龍'}),
                        Pokemon.create({name:'妙蛙花'}),
                        Pokemon.create({name:'比雕'}),
                        Skill.create({name:'飛行'}),
                        Skill.create({name:'閃電'}),
                        Skill.create({name:'噴火'}),
                        Skill.create({name:'撞擊'}),
                        Skill.create({name:'叫聲'}),
                    ]);
}).then((value)=>{
    //綁定飼主與神奇寶貝

    return Promise.all([value[0].setRootPokeMon(value[2]),
                        value[2].setOwner(value[0]),
                        value[3].setOwner(value[0]),
                        value[1].setRootPokeMon(value[4]),
                        value[4].setOwner(value[1]),
                        value[5].setOwner(value[1]),
                        value[6].setOwner(value[1]),
                        value[7].setOwner(value[1]),
                        //這階段新增的

                        value[8].setPokemons([value[5],value[7]]),
                        value[9].addPokemon(value[2]),
                        value[3].setSkills([value[11],value[12]])
                    ]);
← Sequelize - NodeJS MySQL ORM module 實戰二:CRUD Golang - 初探&Sort練習 →
 
comments powered by Disqus