为受约束的关联起别名

时间:2019-02-20 11:38:26

标签: sequelize.js

我正在将现有的Web应用程序从PHP移植到Node,并使用Sequelize来描述现有的数据表。该端口将分阶段完成,因此将无法适应Sequelize的首选命名约定或表安排,但我相信可以充分描述现有的表定义,而且如果遇到困难,总会有Sequelize的原始查询功能。

其中一个表具有通过表的关联。 venues必须具有属性regions,该属性是属于region类别并与场所实例相关联的标记的数组。

  • 穿透表为venue_relatesto_tags
  • venue.id匹配venue_relatesto_tags.venue_id
  • venue_relatesto_tags.tag_id匹配tags.id
  • tags.category_id匹配tag_categories.id
  • tag_category.category包含一个字符串,例如region

现有的PHP:

public static function findOneByVenueWithCategory(PDO $pdo, Venue $venue, $category) {
        $sql = '    SELECT tags.* FROM venue_relatesto_tags
                    JOIN tags on venue_relatesto_tags.tag_id = tags.id
                    JOIN tag_categories on tag_categories.id = tags.tag_category_id
                    WHERE venue_relatesto_tags.venue_id = :venue_id
                        AND tag_categories.category = :category
                    LIMIT 1';
        $s = $pdo->prepare($sql);
        $s->bindParam(':venue_id', $venue->id, PDO::PARAM_INT);
        $s->bindParam(':category', $category, PDO::PARAM_STR);
        return self::findOneByStatement($pdo, $s, 'Tag');
    }

NB:我知道在这种情况下,它仅获得一个标签,这实际上是现有应用程序中的一个错误,该关系通过贯通表以及在应用程序的其他部分中正确地支持具有M个标签的N个场所,则针对特定的标签类别返回多个标签。

在我正在处理的示例中,我有一个“地点”表,该表被标记为属于一个或多个“区域”。

注意:为清楚起见,此处的表def被截断了:

[模式片段]


--
-- Table structure for table `tag_categories`
--

DROP TABLE IF EXISTS `tag_categories`;
CREATE TABLE `tag_categories` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `category` varchar(64) NOT NULL,
  `singular` varchar(255) DEFAULT NULL,
  `plural` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;

--
-- Table structure for table `tags`
--

DROP TABLE IF EXISTS `tags`;
CREATE TABLE `tags` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `description` varchar(255) NOT NULL,
  `tag_category_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=67 DEFAULT CHARSET=utf8;

--
-- Table structure for table `venues`
--

DROP TABLE IF EXISTS `venues`;
CREATE TABLE `venues` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=194 DEFAULT CHARSET=utf8;

我使用了sequelize-auto来生成后续的`defines',并加上了使时间戳静音的功能。

[地点]

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('venues', {
    id: {
      type: DataTypes.INTEGER(10).UNSIGNED,
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
    title: {
      type: DataTypes.STRING(255),
      allowNull: true
    },
    // ...
  }, {
    tableName: 'venues',
    timestamps: false
  });
};

[venues_relatesto_tags]

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('venue_relatesto_tags', {
    id: {
      type: DataTypes.INTEGER(10).UNSIGNED,
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
    venue_id: {
      type: DataTypes.INTEGER(10).UNSIGNED,
      allowNull: true
    },
    tag_id: {
      type: DataTypes.INTEGER(10).UNSIGNED,
      allowNull: true
    }
  }, {
    tableName: 'venue_relatesto_tags',
    timestamps: false
  });
};

[标签]

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('tags', {
    id: {
      type: DataTypes.INTEGER(10).UNSIGNED,
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
    description: {
      type: DataTypes.STRING(255),
      allowNull: false
    },
    tag_category_id: {
      type: DataTypes.INTEGER(10).UNSIGNED,
      allowNull: true
    }
  }, {
    tableName: 'tags',
    timestamps: false
  });
};

[tag_categories]

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('tag_categories', {
    id: {
      type: DataTypes.INTEGER(10).UNSIGNED,
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
    category: {
      type: DataTypes.STRING(64),
      allowNull: false
    },
    // ...
  }, {
    tableName: 'tag_categories',
    timestamps: false
  });
};
function associateSchema(db) {
  db.models.venues.belongsToMany(db.models.tags, {
    through: 'venue_relatesto_tags',
    foreignKey: 'venue_id',
    otherKey: 'tag_id',
    as: 'tags'
  })
  // This association is what I current have to get an appropriate
  // alias to the region tags
  db.models.venues.belongsToMany(db.models.tags, {
    through: 'venue_relatesto_tags',
    foreignKey: 'venue_id',
    otherKey: 'tag_id',
    as: 'regions'
  })
  db.models.tags.belongsTo(db.models.tag_categories, {
    foreignKey: 'tag_category_id',
    as: 'categories'
  })
  // other associations...
  db.sync({ alter: false })
}

然后我定义了以下范围:

function addScopes(db) {
  db.models.events.addScope('defaultScope', {
    include: [
      {
        association: 'venue',
        include: [
          {
            association: 'regions',
            include: [
              {
                association: 'categories',
                attributes: [], // don't need the categories in the result
                where: {
                  // only get region tags
                  category: 'region'
                }
              }
            ],
          }
        ]
      },
    ],
    order: [ 'title' ]
  }, {
    override: true
  })
}

这行得通,因为我在场地实例中最终得到了一系列“区域”。 include有一个as选项,但似乎仅与model一起使用,因此重复关联(tags / regions)似乎是描述的唯一方法这似乎不是很干(更不用说从维护的角度来看可能造成混淆)。

venue表仅具有一个区域标签关联,但是其他表具有多个基于标签的关联,但是针对不同的类别,因此我可以看到我会一次又一次地遇到该关联,并且有难闻的代码气味。

据我所知,使用范围和查询方法的选项对我来说没有什么区别,而且我可以看到我可能选择具有多个范围,这样当我不使用时,不会产生比所需更多的联接不需要数据(或使用替代来限制默认范围)。

我确定有人可能会认为现有的表格结构不是理想的,而是它的遗产,我需要容纳它的脆弱性,而又不能将它们保留在锁,库存和桶中以用于新的实现。一旦遗留代码消失,将有机会重构表结构。

到目前为止,我还没有必要描述逆关系,但这可能是因为到目前为止,我只是在摸索表面并评估Sequelize是否适合我的需求。一种选择是使用原始的MYSQL驱动程序,仅复制来自PHP应用程序的查询,但是其中一方面是PHP应用程序在SQL实现方面不一定是最佳的。

1 个答案:

答案 0 :(得分:2)

因此,您有三个表:venuestagstag_categories。另外,venuestags之间存在N:M关系,该关系由穿透表(有时也称为结点表或联合表)venue_relatesto_tags处理。 tag_categoriestags之间还有一个1:N的关系(这意味着每个标签都具有一个类别,并且同一类别可以应用于多个标签)。

您只想查询具有至少一个标签为"region" 的场所,并且还希望每个获取的场所对象都有一个额外的字段"regions"这是一个标签数组,仅包含该场所的标签,其类别为"region"

模型定义

要运行我自己的代码,我将对本地postgres数据库使用sequelize进行测试。

出于完整性考虑,我将使用以下模型。它们与您的相同,但是我省略了id字段,并在适用的地方添加了references字段。我的整个代码将放在一个文件中,但是您可以保持每个文件一个表的设置。

const Sequelize = require("sequelize");
const sequelize = new Sequelize("postgres://postgres@localhost:5432/testdb", { dialect: "postgres" });

const Venue = sequelize.define('venues', {
    title: Sequelize.STRING(255)
}, { tableName: 'venues', timestamps: false });

const Tag = sequelize.define('tags', {
    description: Sequelize.STRING(255),
    tag_category_id: {
        type: Sequelize.INTEGER(10).UNSIGNED,
        references: { model: "tag_categories", key: "id" }
    }
}, { tableName: 'tags', timestamps: false });

const TagCategory = sequelize.define('tag_categories', {
    category: Sequelize.STRING(64)
}, { tableName: 'tag_categories', timestamps: false });

const Venue_Tag = sequelize.define('venue_relatesto_tags', {
    venue_id: {
        type: Sequelize.INTEGER(10).UNSIGNED,
        references: { model: "venues", key: "id" }
    },
    tag_id: {
        type: Sequelize.INTEGER(10).UNSIGNED,
        references: { model: "tags", key: "id" }
    }
}, { tableName: 'venue_relatesto_tags', timestamps: false });

关系

您使用别名在表之间建立了一些复杂的关系。最糟糕的是在同一个模型之间使用不同的别名创建两个“多对多”关系。我了解您为何基于目标尝试了这一点,但至少可以这样说,这非常令人困惑。所以我不会那样做。获取所需字段的解决方案将有所不同:与其尝试使序列化本身生成所需字段名称的确切结构,不如使用序列化获取数据并在以后使用纯JavaScript使它们一致。 >

Venue.belongsToMany(Tag, { through: Venue_Tag, foreignKey: 'venue_id', otherKey: 'tag_id' });
Tag.belongsToMany(Venue, { through: Venue_Tag, foreignKey: 'tag_id', otherKey: 'venue_id' });
TagCategory.hasMany(Tag, { foreignKey: 'tag_category_id' });
Tag.belongsTo(TagCategory, { foreignKey: 'tag_category_id' });

使用测试值填充数据库

此部分仅出于完整性的目的,向您展示如何使用伪值填充我自己的数据库,以及测试我自己的代码。我决定创建两个不同的tag_categories,其中一个叫做"Region"。另外,我创建了四个标签,其中两个分类为"Region",另外两个venues,第一个包含所有标签,第二个没有区域分类标签。见下文。

function setup() {
  return sequelize.sync({ force: true })
    .then(() => Promise.all([
        TagCategory.create({ category: "Region" }),
        TagCategory.create({ category: "Other Category" })
    ]))
    .spread((region, otherCategory) => Promise.all([
        Tag.create({ description: "Tag 1", tag_category_id: otherCategory.id }),
        Tag.create({ description: "Tag 2", tag_category_id: otherCategory.id }),
        Tag.create({ description: "Tag 3", tag_category_id: region.id }),
        Tag.create({ description: "Tag 4", tag_category_id: region.id })
    ]))
    .spread((tag1, tag2, tag3, tag4) => Promise.all([
        Venue.create({ title: "Venue 1" }).tap(venue => venue.setTags([tag1, tag2, tag3, tag4])),
        Venue.create({ title: "Venue 2" }).tap(venue => venue.setTags([tag1, tag2]))
    ]));
}

查询

以下查询将获取所有具有至少一个标签分类为"Reason"的场所,仅包括以下标签:

Venue.findAll({
  include: {
    model: Tag,
    required: true,
    include: {
      model: TagCategory,
      required: true,
      where: {
        category: "Region"
      }
    }
  }
})

将其格式化为所需的结构

您说过要将包含这些标签的字段命名为regions,因此我们只使用JavaScript对其进行修复。我们也可能会删除不需要的字段"venue_relatesto_tags"tag_category。代码:

setup()
    .then(() => Venue.findAll({
        include: {
            model: Tag,
            required: true,
            include: {
                model: TagCategory,
                required: true,
                where: {
                    category: "Region"
                }
            }
        }
    }))
    .then(venues => {
        venues = venues.map(venue => venue.toJSON());
        for (const venue of venues) {
            venue.regions = venue.tags;
            delete venue.tags;
            for (const tag of venue.regions) {
                delete tag.venue_relatesto_tags;
                delete tag.tag_category;
            }
        }
        return venues;
    })
    .then(result => console.log(JSON.stringify(result, null, 2)))
    .finally(() => sequelize.close());

结果

[
  {
    "id": 1,
    "title": "Venue 1",
    "regions": [
      {
        "id": 4,
        "description": "Tag 4",
        "tag_category_id": 1
      },
      {
        "id": 3,
        "description": "Tag 3",
        "tag_category_id": 1
      }
    ]
  }
]

保留序列化实例

上面的代码将查询结果转换为纯JSON,以便自由地操纵其字段。如果仍然需要序列化实例,则另一种选择是在Venue模型中使用一个名为"regions"的伪字段。将模型更改为以下内容:

const Venue = sequelize.define('venues', {
    title: Sequelize.STRING(255)
}, {
    tableName: 'venues',
    timestamps: false,
    getterMethods: {
        regions() {
            if (!this.tags) return;
            if (this.tags.length === 0) return [];
            if (!this.tags[0].tag_category) return;
            return this.tags.filter(tag => tag.tag_category.category === 'Region');
        }
    }
});

此外,为了更好的组织,我建议为其创建一个范围(自一开始就已经希望如此):

Venue.addScope('findWithRegions', {
    include: {
        model: Tag,
        required: true,
        include: {
            model: TagCategory,
            required: true,
            where: {
                category: 'Region'
            }
        }
    }
});

查询现在将变为:

setup()
    .then(() => Venue.scope("findWithRegions").findAll())
    .then(result => console.log(JSON.stringify(result, null, 2)))
    .finally(() => sequelize.close());

最终想法

尽管我在上面为您提供了使用伪字段的选择,以便您可以拥有想要的字段而不会丢失sequelize实例,但我认为您应该重新考虑为什么确实需要这种方式。对我而言,同时需要两件事是没有意义的。 Sequelize旨在与数据库进行通信并以直观地映射到数据库中条目的方式将数据作为对象检索。另一方面,您想要一个具有不同名称的额外字段的事实似乎更像是前端要求或与ORM不在同一“抽象层”中的其他内容。我认为您应该尝试以某种方式来组织代码,以便在获取所需数据的意义上通过序列化进行所有查询,但是您应该将它们“符合”为仅在下一步需要的任何结构。并且,在这一步中,您不再需要sequelize实例。为了使事情井井有条,您可以在Venue模型中创建一个辅助方法:

Venue.prototype.toJSONWithRegions = function() {
    const asJSON = this.toJSON();
    asJSON.regions = asJSON.tags;
    delete asJSON.tags;
    for (const tag of asJSON.regions) {
        delete tag.venue_relatesto_tags;
        delete tag.tag_category;
    }
    return asJSON;
};

// call it as follows:
// const myVenueAsJSON = myVenueSequelizeInstance.toJSONWithRegions();

希望这会有所帮助。让我知道您是否需要进一步的帮助。