Sequelize一对一、一对多、多对多学不会你砍我

·  阅读 1327

前期准备

源码地址

数据库链接

这里使用的是本地docker中的镜像,具体映射到宿主机的端口为9000,可以执行如下命令,拉起一个自己的docker镜像:

docker run --name mysql1 -e MYSQL_ONETIME_PASSWORD=123456 -p 9000:3306 -d mysql/mysql-server:latest
复制代码

之后我们通过Sequelize来链接数据库

// index.js
const { Sequelize } = require('sequelize')

const sequelize = new Sequelize('test1', "root", "123456", {
  dialect: 'mysql',
  host: "127.0.0.1",
  protocol: "http",
  port: 9000
})


try {
    // 验证数据库是否链接成功
    await sequelize.authenticate()
} catch (err) {}
复制代码

一、一对一

1.1 模拟场景

假设一个用户 (User) 只能有一份出生证明 (BitrhAuth) ,所以这里我们需要创建两张表UserBirthAuth,并且建立他们之间的一对一的关系

1.2 表的创建以及关联关系

1.2.1 定义表

注意这里目前只是定义了表的结构,实际数据库中是没有创建这张表的

// model.js
const { Model, INTEGER, STRING, TEXT } = require('sequelize')

// 通过Model的方式来创建表
class User extends Model {
  static attributes = {
    id: {
      field: 'id',
      autoIncrement: true,
      primaryKey: true,
      type: INTEGER
    },
    name: STRING(255)
  }

  // 这里的表名,就是创建到数据库中的表名
  static tableName = "Buyer"
}

class File extends Model {
  static attributes = {
    id: {
      field: 'id',
      autoIncrement: true,
      primaryKey: true,
      type: INTEGER
    },
    title: STRING(255),
    content: TEXT("medium"),
    ownerId: INTEGER
  }
  static tableName = "Birth_Auth"
}
复制代码

1.2.2 创建表和关联表

使用init方法来创建对应的表,并且通过sequlize参数将模型和数据库进行链接,之后通过hasOne来声明对应的关系

直到使用sync方法,这个时候才向数据库去同步对应的操作

async function init() {
  try {
    await sequelize.authenticate()
    User.init(User.attributes, {
      sequelize,
      tableName: User.tableName
    })

    File.init(File.attributes, {
      sequelize,
      tableName: File.tableName
    })

    // 一对一创建
    // 后续可以通过setUser的方式来创建对应1对1的元素
    // 这里的hasOne其实就是User和File的关联关系了
    // 通过User的静态变量进行保存,可以在创建user的同时对File进行创建
    User.File =  User.hasOne(File, {
      // 这个foreignKey最终是落在File表中的,就是File的配置
      foreignKey: 'ownerId',
      // 这个sourceKey目前代表的是一个
      sourceKey: "id",
      // 这个as非常重要
      // 因为在产生关联关系之后,后续sequelize会增加一个setXXX的方法用于同时创建关联关系
      // 这个时候直接使用的就是这个as的变量
      as: "birth"
    })
  } catch(e) {
      console.log(e) 
  }

  // 直到使用sync才是向数据库同步表的操作~
  await sequelize.sync({ force: !!isForce })
}
复制代码

1.2 创建

完成了上述关联关系之后,我们就来创建一条UserFile之间的数据

// 为已有的用户添加出生证明
async function addInExist() {
  // 一对一创建
  const [user] = await User.findOrCreate({
      where: {
        name: {
          [Op.like]: '%xiaodeng%'
        }
      },
      defaults: {
        name: "xiaodeng"
      }
  })

  const file = await File.create({
      title: `pi essay ${Math.random()}`,
      content: `content ${Math.random()}`
  })

  // 这里就是通过setBirth来将user和file两者进行关联
  await user.setBirth(file)
}

// 通过include model的方式可以同时创建对应一对一、一对多的关系
async function addInCreate() {
  await User.create({
    name: "xiaoliu",
    birth: {
      title: "xiaoliuzhengming",
      content: 'content'
    }
  }, {
    include: [{
      model: File,
      as: 'birth'
    }]
  })
}
复制代码

1.3 查询

在查询的时候和创建的场景一样,通过option参数中的include,来对已有的关联关系进行查询

async function query() {
  // 一对一查询
  // 一对一和一对多查询
  const { rows, count } = await User.findAndCountAll({
    include: [{
      model: File,
      as: 'birth'
    }]
  })
}

/*
查询出的结果如下
[
  {
    "id": 1,
    "name": "xiaodeng",
    "createdAt": "2022-04-05T05:28:08.000Z",
    "updatedAt": "2022-04-05T05:28:08.000Z",
    // 会同时将birth查出来
    "birth": {
      "id": 13,
      "title": "pi essay 0.5358826142709898",
      "content": "content 0.42366009995271936",
      "ownerId": 1,
      "createdAt": "2022-04-05T05:33:41.000Z",
      "updatedAt": "2022-04-05T05:33:41.000Z"
    }
  }
]
*/
复制代码

二、一对多

2.1 模拟场景

假设一个用户 (User) 能买多个不同的手机 (Phone) ,所以这里我们需要创建两张表UserPhone,并且建立他们之间的一对多的关系

2.2 表的创建以及关联关系

2.2.1 定义表

class Phone extends Model {
  static attributes = {
    id: {
      field: 'id',
      autoIncrement: true,
      primaryKey: true,
      type: INTEGER
    },
    name: STRING(255),
    type: STRING(255)
  }

  static tableName = "Phone"
}
复制代码

2.2.2 创建表和关联表

使用init方法来创建对应的表,并且通过sequlize参数将模型和数据库进行链接,之后通过hasMany来声明对应的关系

function init() {
    // 省略1中的部分代码
    // 创建Phone这张表
    Phone.init(Phone.attributes, {
      sequelize,
      tableName: Phone.tableName
    })

    // 一对多的关系
    // 后续可以通过Include的方式查询到对应表中的内容
    User.Phones = User.hasMany(Phone, {
        // sourceKey是User的field
        sourceKey: "id",
        // 这里的foreignKey是Phone中关联的字段
        foreignKey: "ownerId",
        as: "phones"
    })
}
复制代码

2.3 创建

// User已有,这个时候去添加对应的手机id
async function addOne () {
  const [user] = await User.findOrCreate({
    where: {
      id: 1
    }
  })

  // 
  const phones = await Phone.bulkCreate([{
    name: "iphone 13",
    type: "Apple"
  }, {
    name: "Mate 40 Pro",
    type: "HuaWei"
  }])

  // 这里适用setPhones,为什么是Phones?
  // 因为在hasMany创建关联关系中的as就是phones
  // 所以这里也是Phones
  await user.setPhones(phones)
}

async function addInCreate () {
  // 一对多创建
  // 同时创建对应的User和其关联的Phone
  await User.create({
    name: "xiaoze",
    phones: [{
      name: "IPhone 13 pro max",
      type: "Apple"
    }, {
      name: "Mate P30",
      type: "Hua Wei"
    }]
  }, {
    include: [{
      model: Phone,
      as: 'phones'
    }]
  })
}
复制代码

2.4 查询

查询其实和一对一一样,想要查具体的字段,直接对应的使用include,并且关联上相关的model即可

async function query() {
  // 一对一查询
  // 一对一和一对多查询
  const { rows, count } = await User.findAndCountAll({
    where: {
      name: {
        [Op.like]: '%xiaoze%'
      }
    },
    include: [{
      model: File,
      as: 'birth'
    },{
      model: Phone,
      as: 'phones'
    }]
  })
}

/*
[
  {
    "id": 11,
    "name": "xiaoze",
    "createdAt": "2022-04-05T07:29:49.000Z",
    "updatedAt": "2022-04-05T07:29:49.000Z",
    "birth": null,
    "phones": [
      {
        "id": 21,
        "name": "IPhone 13 pro max",
        "type": "Apple",
        "createdAt": "2022-04-05T07:29:49.000Z",
        "updatedAt": "2022-04-05T07:29:49.000Z",
        "ownerId": 11
      },
      {
        "id": 22,
        "name": "Mate P30",
        "type": "Hua Wei",
        "createdAt": "2022-04-05T07:29:49.000Z",
        "updatedAt": "2022-04-05T07:29:49.000Z",
        "ownerId": 11
      }
    ]
  }
]
*/
复制代码

三、多对多

3.1 模拟场景

假设一个演员 (Actor) 能出演多部电影 (Movie) ,而一本电影 (Movie),也可能有多个演员 (Actor) 所以这里我们需要创建两张表MovieActor,因为多对多的关系,需要通过中间表MovieActor来进行维护彼此的关系,所以这次需要三张表。

3.2 表的创建以及关联关系

3.2.1 定义表

class Movie extends Model {
  static attributes = {
    id: {
      field: 'id',
      autoIncrement: true,
      primaryKey: true,
      type: INTEGER
    },
    name: STRING(255),
    fee: INTEGER
  }

  static tableName = "Movie"
}

class Actor extends Model {
  static attributes = {
    id: {
      field: 'id',
      autoIncrement: true,
      primaryKey: true,
      type: INTEGER
    },
    sex: STRING(255),
    name: STRING(255)
  }

  static tableName = 'Actor'
}

class MovieActor extends Model {
  static attributes = {
    actorId: INTEGER,
    movieId: INTEGER
  }

  static tableName = 'MovieActor'
}
复制代码

3.2.2 创建表和关联表

使用init方法来创建对应的表,并且通过sequlize参数将模型和数据库进行链接,之后通过belongsToMany来声明对应的关系

// index.js

function init() {
    // ...省略上述已重复的代码
    // 需要先将上述三张表进行初始化
    Actor.init(Actor.attributes, {
      sequelize,
      tableName: Actor.tableName
    })

    Movie.init(Movie.attributes, {
      sequelize,
      tableName: Movie.tableName
    })

    MovieActor.init(MovieActor.attributes, {
      sequelize,
      tableName: MovieActor.tableName
    })

    // 分别向对面的表申明关系为一对多
    // 这里需要注意,多对多的场景,我们需要通过中间表来处理两者的关联关系
    // 所以这个through,这个中间表的字段是必填
    Actor.belongsToMany(Movie, { through: MovieActor, foreignKey: 'actorId', as: 'movies' })
    Movie.belongsToMany(Actor, { through: MovieActor, foreignKey: 'movieId', as: 'actors' })
}
复制代码

3.1 创建

function add () {
  const actors = await Actor.bulkCreate([{
    name: "wangbaoqiang",
    sex: "male"
  },{
    name: "tongliya",
    sex: "female"
  }, {
    name: "SunHonglei",
    sex: "female"
  }])

  const Movies = await Movie.bulkCreate([{
    name: "Qianfu",
    fee: 100
  }, {
    name: "TangTan",
    fee: 100
  }, {
    name: "newMoview",
    fee: 200
  }])

  // 这里的setActors就是从as里面取的,这个就不赘述
  await Movies[0].setActors(actors.slice(0, 2))
  await Movies[1].setActors([actors[2]])
  await Movies[2].setActors(actors)
}
复制代码

3.2 查询

这里因为是多对多的关系,所以无论是从Actors查询Movie还是从MovieActors都是可以的

function query() {
  const { rows: movies } = await Movie.findAndCountAll({
    include: [{
      model: Actor,
      as: 'actors'
    }]
  })

  const { rows: actors } = await Actor.findAndCountAll({
    include: [{
      model: Movie,
      as: 'movies'
    }]
  })
}
复制代码

四、TS支持

4.1 模型定义部分

在TS支持的部分只需要明确几个点:

  1. 如何把attributes的数据结构能够通过.的方式进行提示

  2. 对于代码中混入的那些getActors,setActors等方法如何提示

其实只需要通过declare进行声明即可~

import { Model, INTEGER, STRING, HasManySetAssociationsMixin, HasManyGetAssociationsMixin } from 'sequelize'

class Actor extends Model {
  static attributes = {
    id: {
      field: 'id',
      autoIncrement: true,
      primaryKey: true,
      type: INTEGER
    },
    sex: STRING(255),
    name: STRING(255)
  }

  static tableName = 'Actor'

  declare id: number;
  declare sex: string;
  declare name: string;

  declare setMovies: HasManySetAssociationsMixin<Movie, number>
  declare getMovies: HasManyGetAssociationsMixin<Movie>

}

class Movie extends Model {
  static attributes = {
    id: {
      field: 'id',
      autoIncrement: true,
      primaryKey: true,
      type: INTEGER
    },
    name: STRING(255),
    fee: INTEGER
  }

  static tableName = "Movie"

  declare id: number;
  declare name: string;
  declare fee: number;

  declare getActors: HasManyGetAssociationsMixin<Actor>
  declare setActors: HasManySetAssociationsMixin<Actor, number>
}

class MovieActor extends Model {
  static attributes = {
    actorId: INTEGER,
    movieId: INTEGER
  }

  static tableName = 'MovieActor'
}


export {
  Actor,
  Movie,
  MovieActor
}
复制代码

4.2 重构演员电影部分的代码

import { Sequelize } from "sequelize";
import { Movie, Actor, MovieActor } from "./models/Actor";

const sequelize = new Sequelize("seq", "root", "123456", {
  dialect: "mysql",
  host: "127.0.0.1",
  protocol: "http",
  port: 9000,
});

async function init() {
  [Movie, Actor, MovieActor].forEach(async (model) => {
    await model.init(model.attributes, {
      sequelize,
      tableName: model.tableName,
    });
  });

  Movie.belongsToMany(Actor, {
    through: MovieActor,
    foreignKey: "movieId",
    as: "actors",
  });
  Actor.belongsToMany(Movie, {
    through: MovieActor,
    foreignKey: "actorId",
    as: "movies",
  });

  return sequelize.sync();
}

async function addData() {
  const actors = await Actor.bulkCreate([
    {
      name: "wangbaoqiang",
      sex: "male",
    },
    {
      name: "tongliya",
      sex: "female",
    },
    {
      name: "SunHonglei",
      sex: "female",
    },
  ]);

  const Movies = await Movie.bulkCreate([
    {
      name: "Qianfu",
      fee: 100,
    },
    {
      name: "TangTan",
      fee: 100,
    },
    {
      name: "newMoview",
      fee: 200,
    },
  ]);

  await Movies[0].setActors(actors.slice(0, 2));
  await Movies[1].setActors([actors[2]]);
  await Movies[2].setActors(actors);
}

async function query() {
  const { rows } = await Movie.findAndCountAll({
    include: [
      {
        model: Actor,
        as: "actors",
      },
    ],
  });

  const jsonArray = rows.map((item) => item.toJSON());
  console.log(JSON.stringify(jsonArray, null, 2));
}

(async () => {
  await init();
  await addData();
  await query();

  await sequelize.close();
})();
复制代码

五、几个Key的简单记忆

这里有几个我们在使用关联关系中用到的Key:foreignKeysourceKeytargetKey(本次没有用到)以及hasXX(One/Many)BelongToXX之间的关系,可以按照下面的来进行总结,本人目前适用。

假设我们要的关系是N对M,那么对应的创建公式可以是:

N.hasXXX(M, {foreignKey: 'M的key', sourceKey: 'N的key', as: 'M的别名,用于查询时候setXX时候用'})

N.belongToXX(M, {foreignKey: 'N的key', sourceKey: 'M的key', as: 'M的别名,用于查询和赋值的时候setXX, getXX用'})

从上面的规律来看belongToXXhasXX基本上就是相反的

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改