-
I have the following model:
In this example there are words, each of which can have any number of tags on them, which for now are stored in a separate table so they can be used for joins. My goal is to be able to construct a filter that returns the list of words that have every tag in some set of tags, for example:
The problem I'm facing is that I only know how to construct the query such that it returns if a word has any one of the requested tags (e.g. if I ask for words with tags "tag1, tag2, tag3" I would get back a word even if it didn't have "tag3" so long as it has either "tag1" or "tag2"). What I have right now:
I would think it should be something like the following, though this of course doesn't work:
Is there anything I can change to get this desired behavior (data model, query, etc.)? Thanks in advance! |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 6 replies
-
Hello @ethankay, In order to solve this puzzle, you can take inspiration from the joining(required:) example: // Fetch books by French authors
let request = Book.joining(required: Book.author.filter(Column("country") == "France")) In your case, you want to fetch words that are joined to tag "foo", but also to tag "bar", and to tag "baz", etc: // Fetch words with tags "foo", "bar", and "bar"
let request = Word
.joining(required: ... "foo")
.joining(required: ... "bar")
.joining(required: ... "baz") This gives: extension Word {
// Association to the tag named `tag`
static func tag(named tag: String) -> HasOneAssociation<Word, TagToWord> {
// Prefer hasOne to hasMany, since there exists at most one row
// in the tagToWord table for a given tag.
hasOne(TagToWord.self, using: ForeignKey(["word_id"]))
.filter(TagToWord.Columns.tag == tag)
// Use one distinct association key per tag, so that we can use several
// of those associations in a single request without any conflict due to
// https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md#refining-association-requests
.forKey("tagToWord_\(tag)")
}
}
extension DerivableRequest where RowDecoder == Word {
/// Returns a request for words tagged with all tags in `tags`
func tagged(_ tags: Set<String>) -> Self {
var request = self
for tag in tags {
request = request.joining(required: Word.tag(named: tag))
}
return request.distinct()
}
} Possible variantextension DerivableRequest where RowDecoder == Word {
/// Returns a request for words tagged with the given tag
func tagged(_ tag: String) -> Self {
joining(required: Word.tag(named: tag))
}
/// Returns a request for words tagged with all tags in `tags`
func tagged(_ tags: Set<String>) -> Self {
tags
.reduce(self, { (request, tag) in request.tagged(tag) })
.distinct()
}
} Usage: try dbQueue.write { db in
try db.execute(sql: """
INSERT INTO word (id, name) VALUES (1, 'A');
INSERT INTO tagToWord (word_id, tag) VALUES (1, 'foo');
INSERT INTO tagToWord (word_id, tag) VALUES (1, 'bar');
INSERT INTO word (id, name) VALUES (2, 'B');
INSERT INTO tagToWord (word_id, tag) VALUES (2, 'foo');
INSERT INTO tagToWord (word_id, tag) VALUES (2, 'bar');
INSERT INTO tagToWord (word_id, tag) VALUES (2, 'baz');
INSERT INTO word (id, name) VALUES (3, 'C');
INSERT INTO tagToWord (word_id, tag) VALUES (3, 'foo');
INSERT INTO tagToWord (word_id, tag) VALUES (3, 'baz');
""")
func test(_ db: Database, tags: Set<String>) throws {
print("Words for tags \(tags):")
let words = try Word.all().tagged(tags).fetchAll(db)
print(words.map(\.name).joined(separator: ", "))
}
try test(db, tags: ["foo"]) // A, B, C
try test(db, tags: ["bar"]) // A, B
try test(db, tags: ["foo", "bar"]) // A, B
try test(db, tags: ["foo", "bar", "baz"]) // B
} |
Beta Was this translation helpful? Give feedback.
Hello @ethankay,
In order to solve this puzzle, you can take inspiration from the joining(required:) example:
In your case, you want to fetch words that are joined to tag "foo", but also to tag "bar", and to tag "baz", etc:
This gives: