Vapor 3 中建立sibling relations的方法
我的项目中有这样一个需求,一个用户可以订阅多个主题,而任意一个主题可以同时被多个用户订阅,这就需要在两个models:User和Topic之间建立sibling relations,也就是many-to-many的关系。
- 先来看看需要对两个models分别作出怎样的设置。
先来构建User和Topic的基本要素:
struct User: SQLiteModel{
var id: Int?
var email: String
var password: String
}
//
struct Topic: SQLiteModel{
var id: Int?
var name: String
}
项目使用SQLite数据库,所以这两个model遵循SQLiteModel这个协议。
为了支持在http通讯中被作为参数传递,以及支持数据库相关配置,还需要遵循另外几个必要的协议,这几个协议都是直接宣称遵循就行,无需添加更多的代码支持,所以代码如下:
struct User: SQLiteModel{
var id: Int?
var email: String
var password: String
}
extension User: Content {}
extension User: Parameter {}
extension User: Migration {}
//
struct Topic: SQLiteModel{
var id: Int?
var name: String
}
extension Topic: Content {}
extension Topic: Parameter {}
extension Topic: Migration {}
2.在Vapor中,建立sibling relations需要额外建立一个model来作为中介,来记录User和Topic这两者之间的关系。在这里将其命名为UserTopicPivot,它需要遵循SQLitePivot协议(因为项目使用SQLite数据库,如果使用其它数据库需要对应更改为相应的协议),所以有:
struct UserTopicPivot: SQLitePivot {
var id: Int?
var userID: Int
var topicID: Int
typealias Left = User
typealias Right = Topic
static let leftIDKey: LeftIDKey = \.userID
static let rightIDKey: RightIDKey = \.topicID
}
这里属性id用于数据库条目的唯一性标记,属性userID和topicID用于记录哪个user和哪个topic关联,剩下的代码则是对SQLitePivot这个协议的必须的代码支持。
为了配合数据库的相关配置,另外还需要遵守Migration协议,并提供额外的定制代码支持,所以有:
struct UserTopicPivot: SQLitePivot {
var id: Int?
var userID: Int
var topicID: Int
typealias Left = User
typealias Right = Topic
static let leftIDKey: LeftIDKey = \.userID
static let rightIDKey: RightIDKey = \.topicID
}
extension UserTopicPivot: Migration {
static func prepare(on connection: SQLiteConnection) -> Future{
return Database.create(self, on: connection, closure: { (builder) in
try addProperties(to: builder)
builder.reference(from: \.userID, to: \User.id)
builder.reference(from: \.topicID, to: \Topic.id)
})
}
}
新增代码的作用是,在数据库准备的过程中,让UserTopicPivot这个中介分别和关联的特定用户和特定主题建立一一对应的关系。
- 有了UserTopicPivot这个中介,现在我们回过头来进一步配置User和Topic这两个model.
struct User: SQLiteModel{
var id: Int?
var email: String
var password: String
}
extension User: Content {}
extension User: Parameter {}
extension User: Migration {}
extension User {
var subscribeTopics: Siblings<User,Topic,UserTopicPivot> {
return siblings()
}
}
struct Topic: SQLiteModel{
var id: Int?
var name: String
}
extension Topic: Content {}
extension Topic: Parameter {}
extension Topic: Migration {}
extension Topic {
var subcribers: Siblings<Topic,User,UserTopicPivot> {
return siblings()
}
}
请注意新增部分的代码,属性subscribeTopics用于查询某个用户订阅了哪些主题,属性subcribers用于查询某个主题被哪些用户订阅了。
- 有个以上3个环节的代码,下面我们就可以来看看是如何使用它们来满足项目需求。
对应于User,我们创建有UserController;而对应于Topic,我们创建有TopicController。让我们为这两个Controller分别添加查询订阅关系的方法代码,而在用户订阅某个主题的方法则放在UserController里::
struct UserController {
func getSubscribeTopics(request: Request) throws -> Future<[Topic]> {
let id = try request.parameters.next(Int.self)
return User.query(on: request).filter(\.id == id).first().flatMap({ (user) in
guard let user = user else {
throw Abort(.notFound, reason: "没有此用户", identifier: nil)
}
return try user.subscribeTopics.query(on: request).all()
})
}
func subscribe(_ req: Request, pair: SiblingPair) throws -> Future<HTTPResponseStatus> {
return Topic.query(on: req).filter(\.name ~= pair.topicName).first().flatMap({ (topic) in
guard let topic = topic else {
throw Abort(.badRequest, reason: "主题不存在", identifier: nil)
}
let pivot = try UserTopicPivot(id: nil, userID: pair.userID, topicID: topic.requireID())
return pivot.save(on: req).transform(to: .created)
})
}
}
}
struct TopicController {
func getTopicSubscriber(_ req: Request) throws -> Future<[User]> {
let name = try req.parameters.next(String.self)
guard let convert = name.removingPercentEncoding else {
throw Abort(.expectationFailed, reason: "主题名称无效", identifier: nil)
}
return Topic.query(on: req).filter(\.name ~= convert).first().flatMap({ (topic) in
guard let topic = topic else {
throw Abort(.badRequest, reason: "主题不存在", identifier: nil)
}
return try topic.subcribers.query(on: req).all()
})
}
}
然后我们就可以在Routes.swift文件中这样使用它们:
public func routes(_ router: Router) throws {
let uCtrller = UserController()
let usersRouter = router.grouped("api","users")
usersRouter.get("subscribetopics",Int.parameter, use: uCtrller.getSubscribeTopics)
usersRouter.post(SiblingPair.self, at: "subscribe", use: uCtrller.subscribe)
let tpCtrller = TopicController()
let topicsRouter = router.grouped("api","topics")
topicsRouter.get("subscribers",String.parameter, use: tpCtrller.getTopicSubscriber)
}
就这样,我们就构建完毕查询订阅关系的API。
假如想查询ID为7的用户订阅了哪些主题,可以使用:
GET http://www.myserver.com:8080/api/users/subcribetopics/7
如果想查询名字为vapor的主题被哪些用户订阅,可以使用:
GET http://www.myserver.com:8080/api/topics/subcribers/vapor
用户订阅主题则可以使用:
POST http://www.myserver.com:8080/api/users/subcribe
//将用户id和订阅主题名称的数据封装成JSON数据,作为httpBody传递到服务器(SiblingPair的作用正是这个)
小结:
在vapor3中配置sibling relations的文章难寻踪迹,我只能搜索到在vapor 2环境下配置的文章,而vapor 2到3的变化比较大,无法照搬。由于Fluent仍然处于3.0 RC的阶段,ORM(Object Relational Mapping)的官方文档仍然缺失,希望这个短文能帮助到有同样需求的开发者。