我想创建一条路由,让用户更新其数据(例如,更改其电子邮件或用户名)。为了确保一个用户不能使用与另一个用户相同的用户名,我想检查数据库中是否已经存在一个具有相同用户名的用户。
我已经使用户名在迁移中唯一。
我有一个看起来像这样的用户模型:
struct User: Content, SQLiteModel, Migration {
var id: Int?
var username: String
var name: String
var email: String
var password: String
var creationDate: Date?
// Permissions
var staff: Bool = false
var superuser: Bool = false
init(username: String, name: String, email: String, password: String) {
self.username = username
self.name = name
self.email = email
self.password = password
self.creationDate = Date()
}
}
这是我要使用的代码段:
func create(_ req: Request) throws -> EventLoopFuture<User> {
return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
// Check if `userRequest.email` already exists
// If if does -> throw Abort(.badRequest, reason: "Email already in use")
// Else -> Go on with creation
let digest = try req.make(BCryptDigest.self)
let hashedPassword = try digest.hash(userRequest.password)
let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)
return persistedUser.save(on: req)
}
}
我可以这样进行操作(请参阅下一个代码段),但是这似乎是一个奇怪的选择,因为在进行更多检查时,例如,它需要大量嵌套。必须执行唯一性(例如,在更新用户的情况下)。
func create(_ req: Request) throws -> EventLoopFuture<User> {
return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
let userID = userRequest.email
return User.query(on: req).filter(\.userID == userID).first().flatMap { existingUser in
guard existingUser == nil else {
throw Abort(.badRequest, reason: "A user with this email already exists")
}
let digest = try req.make(BCryptDigest.self)
let hashedPassword = try digest.hash(userRequest.password)
let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)
return persistedUser.save(on: req)
}
}
}
作为答案之一,我试图添加错误中间件(请参阅下一个片段),但这不能正确捕获错误(也许我在代码中做错了-刚从Vapor开始)。
import Vapor
import FluentSQLite
enum InternalError: Error {
case emailDuplicate
}
struct EmailDuplicateErrorMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
let response: Future<Response>
do {
response = try next.respond(to: request)
} catch is SQLiteError {
response = request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
}
return response.catchFlatMap { error in
if let response = error as? ResponseEncodable {
do {
return try response.encode(for: request)
} catch {
return request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
}
} else {
return request.eventLoop.newFailedFuture(error: error)
}
}
}
}
执行此操作的快速方法是执行类似User.query(on: req).filter(\.email == email).count()
的操作,并在尝试保存之前检查其等于0。
但是,尽管这几乎对每个人都适用,但是您仍然冒着两个用户尝试在同一时间使用相同用户名注册的极端情况的风险-解决此问题的唯一方法是捕获保存失败,请检查是否失败是因为对电子邮件的唯一性约束并将错误返回给用户。但是,即使是大型应用程序,您实际击中的机会也很少。
我将在模型中使用unique
作为字段Migration
,例如:
extension User: Migration {
static func prepare(on connection: SQLiteConnection) -> Future<Void> {
return Database.create(self, on: connection) { builder in
try addProperties(to: builder)
builder.unique(on: \.email)
}
}
}
如果您使用默认的String
作为email
的字段类型,那么您将需要减小它,因为这会创建一个对于VARCHAR(255)
键来说太大的字段UNIQUE
。然后,我将使用一些自定义Middleware
来捕获使用同一封电子邮件再次尝试保存记录时出现的错误。
struct DupEmailErrorMiddleware: Middleware
{
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response>
{
let response: Future<Response>
do {
response = try next.respond(to: request)
} catch is MySQLError {
// needs a bit more sophistication to check the specific error
response = request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
}
return response.catchFlatMap
{
error in
if let response = error as? ResponseEncodable
{
do
{
return try response.encode(for: request)
}
catch
{
return request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
}
} else
{
return request.eventLoop.newFailedFuture(error: error )
}
}
}
}