简体   繁体   English

在 Vapor 4 中使用协议的通用流利查询

[英]Generic fluent query with protocol in Vapor 4

I have several models that have a date field with the same name like this:我有几个具有相同名称的日期字段的模型,如下所示:

final class ModelA: Model {

static let schema = "model_a"

@Timestamp(key: "modification_date", on: .update, format: .unix)
var modificationDate: Date?

}

final class ModelB: Model {

static let schema = "model_b"

@Timestamp(key: "modification_date", on: .update, format: .unix)
var modificationDate: Date?

}

final class ModelC: Model {

static let schema = "model_c"

@Timestamp(key: "modification_date", on: .update, format: .unix)
var modificationDate: Date?

}

At some point in my code, I would like to get recently changed models.在我的代码中的某个时刻,我想获得最近更改的模型。 So I could write something like this:所以我可以写这样的东西:

func getRecentlyChangedModelAs(on db: Database) async throws -> [ModelA] {
   return try await ModelA.query(on: db)
                          .group(.and) { group in
                             group.filter(\.$modificationDate != nil)
                             group.filter(\.$modificationDate > Date.now.addingTimeInterval(TimeInterval(-3600)))
                          }
                          .all()
}

func getRecentlyChangedModelBs(on db: Database) async throws -> [ModelB] {
   return try await ModelB.query(on: db)
                          .group(.and) { group in
                             group.filter(\.$modificationDate != nil)
                             group.filter(\.$modificationDate > Date.now.addingTimeInterval(TimeInterval(-3600)))
                          }
                          .all()
}

func getRecentlyChangedModelCs(on db: Database) async throws -> [ModelC] {
   return try await ModelC.query(on: db)
                          .group(.and) { group in
                             group.filter(\.$modificationDate != nil)
                             group.filter(\.$modificationDate > Date.now.addingTimeInterval(TimeInterval(-3600)))
                          }
                          .all()
}

As you can see, there is a lot of code duplication.如您所见,有很多代码重复。 In order to avoid this, I tried to use a protocol and a generic query instead:为了避免这种情况,我尝试使用协议和通用查询:

protocol ModifiableModel: Model, Content {
    var modificationDate: Date? { set get }
}

final class ModelA: ModifiableModel {

static let schema = "model_a"

@Timestamp(key: "modification_date", on: .update, format: .unix)
var modificationDate: Date?

}

etc...

func getRecentlyChangedModel<T: ModifiableModel>(on db: Database) async throws -> [T] {
   return try await T.query(on: db)
                     .group(.and) { group in
                        group.filter(\.$modificationDate != nil) // Errors: 'nil' is not compatible with expected argument type 'KeyPath<Right, RightField>', Generic parameter 'RightField' could not be inferred
                        group.filter(\.$modificationDate > Date.now.addingTimeInterval(TimeInterval(-3600))) // Errors: Cannot convert value of type 'Date' to expected argument type 'KeyPath<Right, RightField>', No exact matches in call to instance method 'filter'
                      }
                      .all()

Unfortunately, this causes errors in the filter of the query.不幸的是,这会导致查询过滤器出错。 I do not quite understand these errors but I found out that without the filter, everything compiles just fine.我不太明白这些错误,但我发现没有过滤器,一切都编译得很好。 My best guess would be that the errors have something to do with the key paths I try to use within the filter.我最好的猜测是错误与我尝试在过滤器中使用的关键路径有关。

Does anyone know what the problem is or how to fix it?有谁知道问题是什么或如何解决? Or is there another approach that I can use to avoid this kind of code duplication?还是有另一种方法可以用来避免这种代码重复? Any help would be much appreciated!任何帮助将非常感激!

I don't think this approach can be made to work.我认为这种方法行不通。 The clue is in the error message you get by putting T in the filter as in:线索在您通过将T放入过滤器中得到的错误消息中,如下所示:

group.filter(\T.$modificationDate != nil)

Gives error:给出错误:

Generic parameter 'Field' could not be inferred and Value of type 'T' has no member '$modificationDate'无法推断通用参数“字段”并且类型“T”的值没有成员“$modificationDate”

The problem is due to the fact that filter is expecting a property defined with a field wrapper (in this case, @Timestamp ).问题是由于filter期望使用字段包装器定义的属性(在本例中为@Timestamp )。 However, as you have probably discovered putting:但是,正如您可能已经发现的那样:

protocol ModifiableModel {
    @Timestamp(key: "modification_date", on: .update, format: .unix)
var modificationDate: Date?
}

results in an error message that you cannot define variables with wrappers in protocols.导致您无法在协议中使用包装器定义变量的错误消息。 Since your approach depends on this being the case, I don't think it can be made to work.由于您的方法取决于这种情况,因此我认为它无法正常工作。

EDIT:编辑:

I forgot to add that you should be able to simplify your code to just filtering on the value.我忘了补充一点,您应该能够将代码简化为仅对值进行过滤。 There shouldn't be any need to test for non-nil first.不应该首先测试非零。 Try:尝试:

func getRecentlyChangedModelAs(on db: Database) async throws -> [ModelA] {
   return try await ModelA.query(on: db)
                          .filter(\.$modificationDate > Date.now.addingTimeInterval(TimeInterval(-3600)))
                          .all()
}

If you are getting the wrong result, then check your migration to make sure there isn't something wrong here.如果您得到错误的结果,请检查您的迁移以确保这里没有问题。

As it turns out, there is a solution, even two actually.事实证明,有一个解决方案,实际上甚至有两个。 They are not perfect in the sense that a small part of the code still needs to be duplicated, but it's still much better than any alternative I know.它们并不完美,因为一小部分代码仍然需要复制,但它仍然比我知道的任何替代方案要好得多。

This is the first way:这是第一种方式:

protocol ModifyableModel: Model {
    var modified: KeyPath<Self, TimestampProperty<Self, UnixTimestampFormat>> { get }
}

extension ModifyableModel {
    func getRecentlyModifiedModels(on db: Database) async throws -> [Self] {
        try await Self.query(on: db)
            .filter(Self().modified > Date.now.addingTimeInterval(TimeInterval(-3600))
            .all()
    }
}

extension ModelA: ModifyableModel {
    var modified: KeyPath<User, TimestampProperty<User, UnixTimestampFormat>> {
        return \.$modificationDate
    }
}

As you can see, the variable modified needs to be implemented separately for every Model that conforms to ModifyableModel .如您所见,需要为每个符合ModifyableModel的 Model 单独实现变量modified Just calling it modificationDate instead doesn't work.只是将其称为modificationDate是行不通的。 It's not perfect but it's a small price to pay.它并不完美,但付出的代价很小。 If anyone figures out a way to avoid this, please let me know.如果有人想办法避免这种情况,请告诉我。

Anyway, here is the other way of doing it.无论如何,这是另一种方法。 Personally, I think it's even a little bit more elegant without the KeyPath:就个人而言,我认为没有 KeyPath 会更加优雅:

protocol ModifyableModel: Model {
    var modified: TimestampProperty<Self, UnixTimestampFormat> { get }
}

func getRecentlyModifiedModels<T: ModifyableModel>(on db: Database) async throws -> [T] {
    return try await T.query(on: db)
        .filter(\T.modified > Date.now.addingTimeInterval(TimeInterval(-3600))
        .all()
}

extension ModelA: ModifyableModel {
    var modified: TimestampProperty<User, UnixTimestampFormat> {
        return self.$modificationDate
    }
}

Thank you, Mahdi BM @ Discord, for your help!谢谢您,Mahdi BM @ Discord,您的帮助!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM