Define and query relationships between record types in GRDB
Associations are connections between record types that mirror the foreign key relationships in your database schema. They enable efficient joins, prefetching, and filtering across related tables.
Without associations, loading related data requires multiple queries:
// Without associations: N+1 queries problemlet books = try Book.fetchAll(db)let bookInfos = try books.map { book -> BookInfo in let author = try Author.fetchOne(db, id: book.authorId) // One query per book! return BookInfo(book: book, author: author)}
With associations, GRDB generates optimized SQL with joins:
// With associations: Single querystruct BookInfo: Decodable, FetchableRecord { var book: Book var author: Author}let bookInfos = try Book .including(required: Book.author) .asRequest(of: BookInfo.self) .fetchAll(db)// SELECT book.*, author.* // FROM book // JOIN author ON author.id = book.authorId
migrator.registerMigration("Books and Authors") { db in try db.create(table: "author") { t in t.autoIncrementedPrimaryKey("id") t.column("name", .text).notNull() } try db.create(table: "book") { t in t.autoIncrementedPrimaryKey("id") t.belongsTo("author", onDelete: .cascade).notNull() t.column("title", .text).notNull() }}
This creates a book.authorId column with a foreign key to author.id.
extension Book { static let author = belongsTo(Author.self) var author: QueryInterfaceRequest<Author> { request(for: Book.author) }}// Fetch the author of a booklet book: Book = ...let author = try book.author.fetchOne(db) // Author?
struct BookInfo: Decodable, FetchableRecord { var book: Book var author: Author}let bookInfos = try Book .including(required: Book.author) .asRequest(of: BookInfo.self) .fetchAll(db)
// Books by French authorslet books = try Book .joining(required: Book.author .filter { $0.country == "France" }) .fetchAll(db)
extension Author { static let books = hasMany(Book.self) var books: QueryInterfaceRequest<Book> { request(for: Author.books) }}// Fetch all books by an authorlet author: Author = ...let books = try author.books.fetchAll(db) // [Book]// Filter the associated bookslet novels = try author.books .filter { $0.genre == "novel" } .order(\.$publishedAt.desc) .fetchAll(db)
struct AuthorInfo: Decodable, FetchableRecord { var author: Author var books: [Book]}// Fetch authors with all their bookslet authorInfos = try Author .including(all: Author.books) .asRequest(of: AuthorInfo.self) .fetchAll(db)// SELECT author.* FROM author// SELECT book.* FROM book WHERE authorId IN (...)
// Annotate with book countstruct AuthorInfo: Decodable, FetchableRecord { var author: Author var bookCount: Int}let authorInfos = try Author .annotated(with: Author.books.count) .asRequest(of: AuthorInfo.self) .fetchAll(db)
struct CountryInfo: Decodable, FetchableRecord { var country: Country var demographics: Demographics?}let countryInfos = try Country .including(optional: Country.demographics) .asRequest(of: CountryInfo.self) .fetchAll(db)
Joins and includes the associated record. Excludes base records without a match (INNER JOIN).
struct BookInfo: Decodable, FetchableRecord { var book: Book var author: Author // Required, never nil}let bookInfos = try Book .including(required: Book.author) .asRequest(of: BookInfo.self) .fetchAll(db)// SELECT book.*, author.*// FROM book// JOIN author ON author.id = book.authorId
Joins and includes the associated record. Includes base records even without a match (LEFT JOIN).
struct BookInfo: Decodable, FetchableRecord { var book: Book var author: Author? // Optional, may be nil}let bookInfos = try Book .including(optional: Book.author) .asRequest(of: BookInfo.self) .fetchAll(db)// SELECT book.*, author.*// FROM book// LEFT JOIN author ON author.id = book.authorId
Includes all associated records using a separate query.
struct AuthorInfo: Decodable, FetchableRecord { var author: Author var books: [Book]}let authorInfos = try Author .including(all: Author.books) .asRequest(of: AuthorInfo.self) .fetchAll(db)// SELECT author.* FROM author// SELECT book.* FROM book WHERE authorId IN (...)
Left join and adds specific columns (may be NULL).
struct BookInfo: Decodable, FetchableRecord { var book: Book var authorName: String?}let bookInfos = try Book .annotated(withOptional: Book.author.select(\.$name)) .asRequest(of: BookInfo.self) .fetchAll(db)