SQLite.swift
SQLite.swiftは「A type-safe, Swift-language layer over SQLite3」として開発された、Swift専用のSQLiteデータベースアクセスライブラリです。SQLite3の薄いラッパーとして機能し、Swiftの型安全性とエクスプレッシブな構文を活用したデータベース操作を提供します。iOS、macOS、watchOS、tvOSのすべてのAppleプラットフォームに対応し、Core DataやRealm等の重厚なフレームワークに対する軽量な代替手段として、モバイルアプリ開発やマックアプリケーション開発において広く採用されています。
GitHub概要
stephencelis/SQLite.swift
A type-safe, Swift-language layer over SQLite3.
トピックス
スター履歴
ライブラリ
SQLite.swift
概要
SQLite.swiftは「A type-safe, Swift-language layer over SQLite3」として開発された、Swift専用のSQLiteデータベースアクセスライブラリです。SQLite3の薄いラッパーとして機能し、Swiftの型安全性とエクスプレッシブな構文を活用したデータベース操作を提供します。iOS、macOS、watchOS、tvOSのすべてのAppleプラットフォームに対応し、Core DataやRealm等の重厚なフレームワークに対する軽量な代替手段として、モバイルアプリ開発やマックアプリケーション開発において広く採用されています。
詳細
SQLite.swift 2025年版は、Swift 5.9以降の最新機能(async/await、Property Wrappers、Result Builder等)を活用し、モダンなSwift開発パラダイムに最適化されています。型安全なクエリビルダー、プリペアドステートメント、トランザクション管理、マイグレーション機能を包括的にサポートし、企業レベルのiOSアプリ開発に必要な信頼性とパフォーマンスを提供します。SwiftUIとの自然な統合、Combine対応、Swift Package Manager完全対応により、現代のiOS開発ワークフローにシームレスに統合可能です。
主な特徴
- 型安全性: Swiftの型システムを活用した完全型安全なクエリ構築
- 軽量設計: SQLite3の薄いラッパーで最小限のオーバーヘッド
- 全プラットフォーム対応: iOS、macOS、watchOS、tvOSサポート
- モダンSwift: async/await、Property Wrappers等の最新機能対応
- クエリビルダー: 直感的で読みやすいクエリ構築API
- マイグレーション: データベーススキーマ変更の自動管理
メリット・デメリット
メリット
- SQLite3の高速性を保持しつつSwiftの型安全性を享受
- Core DataやRealmに比べて軽量で学習コストが低い
- SQLに近い記述でデータベース操作の透明性が高い
- 全Appleプラットフォームでの一貫した開発体験
- Swift Package Managerによる簡単な導入とバージョン管理
- プリペアドステートメントによる高いセキュリティ
デメリット
- Swift/Appleプラットフォーム専用でクロスプラットフォーム対応なし
- Core Dataのような高度なオブジェクトマッピング機能は提供されない
- 複雑なリレーションシップ管理は手動実装が必要
- 大規模データセットでのパフォーマンス最適化は開発者次第
- バックグラウンド処理やマルチスレッド対応は注意が必要
- NoSQL的な柔軟性はSQLiteの制約に依存
参考ページ
書き方の例
セットアップ
// Package.swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "SQLiteSwiftExample",
platforms: [
.iOS(.v15),
.macOS(.v12),
.watchOS(.v8),
.tvOS(.v15)
],
dependencies: [
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.3")
],
targets: [
.target(
name: "SQLiteSwiftExample",
dependencies: [
.product(name: "SQLite", package: "SQLite.swift")
]
),
.testTarget(
name: "SQLiteSwiftExampleTests",
dependencies: ["SQLiteSwiftExample"]
)
]
)
// iOS Xcodeプロジェクトの場合
// File -> Add Package Dependencies -> https://github.com/stephencelis/SQLite.swift.git
import SQLite
import Foundation
基本的な使い方
import SQLite
import Foundation
// データベース接続とテーブル定義
class UserDatabase {
private var db: Connection?
// テーブル定義
private let users = Table("users")
private let id = Expression<Int64>("id")
private let name = Expression<String>("name")
private let email = Expression<String>("email")
private let age = Expression<Int>("age")
private let isActive = Expression<Bool>("is_active")
private let createdAt = Expression<Date>("created_at")
private let profile = Expression<String?>("profile") // JSON文字列として保存
// 投稿テーブル
private let posts = Table("posts")
private let postId = Expression<Int64>("post_id")
private let userId = Expression<Int64>("user_id")
private let title = Expression<String>("title")
private let content = Expression<String>("content")
private let published = Expression<Bool>("published")
private let publishedAt = Expression<Date?>("published_at")
private let viewCount = Expression<Int>("view_count")
init() {
do {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let dbPath = "\(path)/userdb.sqlite3"
db = try Connection(dbPath)
print("Database connected at: \(dbPath)")
createTables()
} catch {
print("Database connection failed: \(error)")
}
}
// テーブル作成
private func createTables() {
do {
// ユーザーテーブル作成
try db?.run(users.create(ifNotExists: true) { table in
table.column(id, primaryKey: .autoincrement)
table.column(name)
table.column(email, unique: true)
table.column(age)
table.column(isActive, defaultValue: true)
table.column(createdAt, defaultValue: Date())
table.column(profile)
})
// 投稿テーブル作成
try db?.run(posts.create(ifNotExists: true) { table in
table.column(postId, primaryKey: .autoincrement)
table.column(userId)
table.column(title)
table.column(content)
table.column(published, defaultValue: false)
table.column(publishedAt)
table.column(viewCount, defaultValue: 0)
table.foreignKey(userId, references: users, id, delete: .cascade)
})
// インデックス作成
try db?.run(users.createIndex(email, ifNotExists: true))
try db?.run(posts.createIndex(userId, ifNotExists: true))
try db?.run(posts.createIndex(published, ifNotExists: true))
print("Tables created successfully")
} catch {
print("Create table error: \(error)")
}
}
}
// ユーザーモデル
struct User {
let id: Int64?
let name: String
let email: String
let age: Int
let isActive: Bool
let createdAt: Date
let profile: UserProfile?
init(id: Int64? = nil, name: String, email: String, age: Int, isActive: Bool = true, createdAt: Date = Date(), profile: UserProfile? = nil) {
self.id = id
self.name = name
self.email = email
self.age = age
self.isActive = isActive
self.createdAt = createdAt
self.profile = profile
}
}
// ユーザープロフィール(JSON)
struct UserProfile: Codable {
let bio: String?
let website: String?
let location: String?
let skills: [String]
init(bio: String? = nil, website: String? = nil, location: String? = nil, skills: [String] = []) {
self.bio = bio
self.website = website
self.location = location
self.skills = skills
}
}
// 投稿モデル
struct Post {
let id: Int64?
let userId: Int64
let title: String
let content: String
let published: Bool
let publishedAt: Date?
let viewCount: Int
init(id: Int64? = nil, userId: Int64, title: String, content: String, published: Bool = false, publishedAt: Date? = nil, viewCount: Int = 0) {
self.id = id
self.userId = userId
self.title = title
self.content = content
self.published = published
self.publishedAt = publishedAt
self.viewCount = viewCount
}
}
// ユーザーサービス拡張
extension UserDatabase {
// ユーザー作成
func createUser(_ user: User) throws -> Int64 {
guard let db = db else { throw DatabaseError.connectionFailed }
let profileJSON = try user.profile?.toJSONString()
let insert = users.insert(
name <- user.name,
email <- user.email,
age <- user.age,
isActive <- user.isActive,
createdAt <- user.createdAt,
profile <- profileJSON
)
let rowid = try db.run(insert)
print("Created user with ID: \(rowid)")
return rowid
}
// 全ユーザー取得
func getAllUsers() throws -> [User] {
guard let db = db else { throw DatabaseError.connectionFailed }
var result: [User] = []
for user in try db.prepare(users.order(createdAt.desc)) {
let userProfile = try? UserProfile.fromJSONString(user[profile])
result.append(User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
))
}
return result
}
// ID指定でユーザー取得
func getUserById(_ userId: Int64) throws -> User? {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = users.filter(id == userId)
guard let user = try db.pluck(query) else { return nil }
let userProfile = try? UserProfile.fromJSONString(user[profile])
return User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
)
}
// メールアドレスでユーザー検索
func getUserByEmail(_ userEmail: String) throws -> User? {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = users.filter(email == userEmail)
guard let user = try db.pluck(query) else { return nil }
let userProfile = try? UserProfile.fromJSONString(user[profile])
return User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
)
}
// ユーザー更新
func updateUser(_ userId: Int64, name: String, email: String, age: Int, profile: UserProfile?) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let userToUpdate = users.filter(id == userId)
let profileJSON = try profile?.toJSONString()
let update = userToUpdate.update(
self.name <- name,
self.email <- email,
self.age <- age,
self.profile <- profileJSON
)
let changes = try db.run(update)
return changes > 0
}
// ユーザーのアクティブ状態変更
func setUserActive(_ userId: Int64, active: Bool) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let userToUpdate = users.filter(id == userId)
let update = userToUpdate.update(isActive <- active)
let changes = try db.run(update)
return changes > 0
}
// ユーザー削除
func deleteUser(_ userId: Int64) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let userToDelete = users.filter(id == userId)
let changes = try db.run(userToDelete.delete())
return changes > 0
}
// 名前で検索
func searchUsersByName(_ searchTerm: String) throws -> [User] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = users.filter(name.like("%\(searchTerm)%")).order(name)
var result: [User] = []
for user in try db.prepare(query) {
let userProfile = try? UserProfile.fromJSONString(user[profile])
result.append(User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
))
}
return result
}
// 年齢範囲で検索
func getUsersByAgeRange(min: Int, max: Int) throws -> [User] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = users.filter(age >= min && age <= max).order(age, name)
var result: [User] = []
for user in try db.prepare(query) {
let userProfile = try? UserProfile.fromJSONString(user[profile])
result.append(User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
))
}
return result
}
// アクティブユーザー数カウント
func getActiveUserCount() throws -> Int {
guard let db = db else { throw DatabaseError.connectionFailed }
return try db.scalar(users.filter(isActive == true).count)
}
// 総ユーザー数
func getTotalUserCount() throws -> Int {
guard let db = db else { throw DatabaseError.connectionFailed }
return try db.scalar(users.count)
}
}
// 使用例
func demonstrateBasicOperations() {
let database = UserDatabase()
do {
// ユーザー作成
let profile1 = UserProfile(bio: "iOS Developer", website: "https://example.com", location: "Tokyo", skills: ["Swift", "iOS", "SwiftUI"])
let user1 = User(name: "Alice Johnson", email: "[email protected]", age: 28, profile: profile1)
let userId1 = try database.createUser(user1)
let profile2 = UserProfile(bio: "Backend Developer", location: "Osaka", skills: ["Swift", "Vapor", "PostgreSQL"])
let user2 = User(name: "Bob Smith", email: "[email protected]", age: 32, profile: profile2)
let userId2 = try database.createUser(user2)
let user3 = User(name: "Charlie Brown", email: "[email protected]", age: 25)
let userId3 = try database.createUser(user3)
print("Created users: \(userId1), \(userId2), \(userId3)")
// 全ユーザー取得
let allUsers = try database.getAllUsers()
print("\n=== All Users ===")
for user in allUsers {
print("ID: \(user.id ?? 0), Name: \(user.name), Email: \(user.email), Age: \(user.age)")
if let profile = user.profile {
print(" Bio: \(profile.bio ?? "N/A"), Skills: \(profile.skills.joined(separator: ", "))")
}
}
// 特定ユーザー取得
if let retrievedUser = try database.getUserById(userId1) {
print("\n=== Retrieved User ===")
print("Retrieved: \(retrievedUser.name) (\(retrievedUser.email))")
}
// ユーザー更新
let updatedProfile = UserProfile(bio: "Senior iOS Developer", website: "https://alice-dev.com", location: "Tokyo", skills: ["Swift", "iOS", "SwiftUI", "Combine"])
let updated = try database.updateUser(userId1, name: "Alice Johnson Smith", email: "[email protected]", age: 29, profile: updatedProfile)
if updated {
print("\n=== User Updated ===")
if let updatedUser = try database.getUserById(userId1) {
print("Updated: \(updatedUser.name) (\(updatedUser.email))")
}
}
// 名前検索
let searchResults = try database.searchUsersByName("Alice")
print("\n=== Search Results ===")
for user in searchResults {
print("Found: \(user.name) (\(user.email))")
}
// 年齢範囲検索
let ageRangeUsers = try database.getUsersByAgeRange(min: 25, max: 30)
print("\n=== Users aged 25-30 ===")
for user in ageRangeUsers {
print("\(user.name): \(user.age) years old")
}
// 統計情報
let totalUsers = try database.getTotalUserCount()
let activeUsers = try database.getActiveUserCount()
print("\n=== Statistics ===")
print("Total users: \(totalUsers)")
print("Active users: \(activeUsers)")
} catch {
print("Error: \(error)")
}
}
投稿機能とJOIN操作
// 投稿サービス拡張
extension UserDatabase {
// 投稿作成
func createPost(_ post: Post) throws -> Int64 {
guard let db = db else { throw DatabaseError.connectionFailed }
let insert = posts.insert(
userId <- post.userId,
title <- post.title,
content <- post.content,
published <- post.published,
publishedAt <- post.publishedAt,
viewCount <- post.viewCount
)
let rowid = try db.run(insert)
print("Created post with ID: \(rowid)")
return rowid
}
// 投稿取得
func getPostById(_ postIdValue: Int64) throws -> Post? {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts.filter(postId == postIdValue)
guard let post = try db.pluck(query) else { return nil }
return Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
)
}
// ユーザーの投稿一覧取得
func getPostsByUser(_ userIdValue: Int64) throws -> [Post] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts.filter(userId == userIdValue).order(postId.desc)
var result: [Post] = []
for post in try db.prepare(query) {
result.append(Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
))
}
return result
}
// 公開済み投稿取得
func getPublishedPosts(limit: Int = 20, offset: Int = 0) throws -> [Post] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts
.filter(published == true)
.order(publishedAt.desc)
.limit(limit, offset: offset)
var result: [Post] = []
for post in try db.prepare(query) {
result.append(Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
))
}
return result
}
// 投稿とユーザー情報結合取得
func getPostsWithAuthors(limit: Int = 20) throws -> [(Post, User)] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts
.join(users, on: users[id] == posts[userId])
.order(posts[postId].desc)
.limit(limit)
var result: [(Post, User)] = []
for row in try db.prepare(query) {
let post = Post(
id: row[posts[postId]],
userId: row[posts[userId]],
title: row[posts[title]],
content: row[posts[content]],
published: row[posts[published]],
publishedAt: row[posts[publishedAt]],
viewCount: row[posts[viewCount]]
)
let userProfile = try? UserProfile.fromJSONString(row[users[profile]])
let user = User(
id: row[users[id]],
name: row[users[name]],
email: row[users[email]],
age: row[users[age]],
isActive: row[users[isActive]],
createdAt: row[users[createdAt]],
profile: userProfile
)
result.append((post, user))
}
return result
}
// 投稿公開
func publishPost(_ postIdValue: Int64) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let postToUpdate = posts.filter(postId == postIdValue)
let update = postToUpdate.update(
published <- true,
publishedAt <- Date()
)
let changes = try db.run(update)
return changes > 0
}
// ビューカウント増加
func incrementViewCount(_ postIdValue: Int64) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let postToUpdate = posts.filter(postId == postIdValue)
let update = postToUpdate.update(viewCount <- viewCount + 1)
let changes = try db.run(update)
return changes > 0
}
// 投稿削除
func deletePost(_ postIdValue: Int64) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let postToDelete = posts.filter(postId == postIdValue)
let changes = try db.run(postToDelete.delete())
return changes > 0
}
// タイトルで投稿検索
func searchPostsByTitle(_ searchTerm: String) throws -> [Post] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts.filter(title.like("%\(searchTerm)%")).order(postId.desc)
var result: [Post] = []
for post in try db.prepare(query) {
result.append(Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
))
}
return result
}
// 人気投稿取得(ビューカウント順)
func getPopularPosts(limit: Int = 10) throws -> [Post] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts
.filter(published == true)
.order(viewCount.desc)
.limit(limit)
var result: [Post] = []
for post in try db.prepare(query) {
result.append(Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
))
}
return result
}
// ユーザー別投稿数
func getUserPostCounts() throws -> [(User, Int)] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = """
SELECT u.*, COUNT(p.post_id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id
ORDER BY post_count DESC
"""
var result: [(User, Int)] = []
for row in try db.prepare(query) {
let userProfile = try? UserProfile.fromJSONString(row[6] as? String)
let user = User(
id: row[0] as! Int64,
name: row[1] as! String,
email: row[2] as! String,
age: row[3] as! Int,
isActive: row[4] as! Bool,
createdAt: row[5] as! Date,
profile: userProfile
)
let postCount = row[7] as! Int
result.append((user, postCount))
}
return result
}
// 投稿統計情報
func getPostStatistics() throws -> (total: Int, published: Int, avgViewCount: Double) {
guard let db = db else { throw DatabaseError.connectionFailed }
let totalPosts = try db.scalar(posts.count)
let publishedPosts = try db.scalar(posts.filter(published == true).count)
let avgViews = try db.scalar(posts.select(viewCount.average)) ?? 0.0
return (total: totalPosts, published: publishedPosts, avgViewCount: avgViews)
}
}
// 使用例
func demonstratePostOperations() {
let database = UserDatabase()
do {
// ユーザー作成
let user = User(name: "John Doe", email: "[email protected]", age: 30)
let userId = try database.createUser(user)
// 投稿作成
let post1 = Post(userId: userId, title: "My First Post", content: "This is the content of my first post.")
let post2 = Post(userId: userId, title: "Another Post", content: "This is another interesting post.")
let post3 = Post(userId: userId, title: "Published Post", content: "This post is published.", published: true, publishedAt: Date())
let postId1 = try database.createPost(post1)
let postId2 = try database.createPost(post2)
let postId3 = try database.createPost(post3)
print("Created posts: \(postId1), \(postId2), \(postId3)")
// 投稿公開
_ = try database.publishPost(postId1)
print("Published post: \(postId1)")
// ビューカウント増加
_ = try database.incrementViewCount(postId1)
_ = try database.incrementViewCount(postId3)
_ = try database.incrementViewCount(postId3)
// 公開済み投稿取得
let publishedPosts = try database.getPublishedPosts()
print("\n=== Published Posts ===")
for post in publishedPosts {
print("Post: \(post.title) (Views: \(post.viewCount))")
}
// 投稿とユーザー情報結合
let postsWithAuthors = try database.getPostsWithAuthors()
print("\n=== Posts with Authors ===")
for (post, author) in postsWithAuthors {
print("Post: \(post.title) by \(author.name)")
}
// 人気投稿
let popularPosts = try database.getPopularPosts(limit: 5)
print("\n=== Popular Posts ===")
for post in popularPosts {
print("Popular: \(post.title) (\(post.viewCount) views)")
}
// 統計情報
let stats = try database.getPostStatistics()
print("\n=== Post Statistics ===")
print("Total posts: \(stats.total)")
print("Published posts: \(stats.published)")
print("Average view count: \(String(format: "%.1f", stats.avgViewCount))")
// ユーザー別投稿数
let userPostCounts = try database.getUserPostCounts()
print("\n=== User Post Counts ===")
for (user, count) in userPostCounts {
print("\(user.name): \(count) posts")
}
} catch {
print("Error: \(error)")
}
}
トランザクションと高度な機能
// トランザクション機能とバッチ処理
extension UserDatabase {
// トランザクション:ユーザーと初期投稿を同時作成
func createUserWithPosts(user: User, initialPosts: [Post]) throws -> (userId: Int64, postIds: [Int64]) {
guard let db = db else { throw DatabaseError.connectionFailed }
var userId: Int64 = 0
var postIds: [Int64] = []
try db.transaction {
// ユーザー作成
let profileJSON = try user.profile?.toJSONString()
let userInsert = users.insert(
name <- user.name,
email <- user.email,
age <- user.age,
isActive <- user.isActive,
createdAt <- user.createdAt,
profile <- profileJSON
)
userId = try db.run(userInsert)
// 初期投稿作成
for postData in initialPosts {
let postInsert = posts.insert(
self.userId <- userId,
title <- postData.title,
content <- postData.content,
published <- postData.published,
publishedAt <- postData.publishedAt,
viewCount <- postData.viewCount
)
let postId = try db.run(postInsert)
postIds.append(postId)
}
}
return (userId: userId, postIds: postIds)
}
// トランザクション:ユーザー間で投稿を移譲
func transferPosts(fromUserId: Int64, toUserId: Int64) throws -> Int {
guard let db = db else { throw DatabaseError.connectionFailed }
var transferCount = 0
try db.transaction {
// 両方のユーザーの存在確認
guard try db.pluck(users.filter(id == fromUserId)) != nil else {
throw DatabaseError.userNotFound(fromUserId)
}
guard try db.pluck(users.filter(id == toUserId)) != nil else {
throw DatabaseError.userNotFound(toUserId)
}
// 投稿を移譲
let postsToTransfer = posts.filter(userId == fromUserId)
transferCount = try db.run(postsToTransfer.update(userId <- toUserId))
}
return transferCount
}
// バッチ処理:複数ユーザーの年齢を一括更新
func batchUpdateUserAges(userAgeUpdates: [(userId: Int64, newAge: Int)]) throws -> Int {
guard let db = db else { throw DatabaseError.connectionFailed }
var totalUpdated = 0
try db.transaction {
for update in userAgeUpdates {
let userToUpdate = users.filter(id == update.userId)
let changes = try db.run(userToUpdate.update(age <- update.newAge))
totalUpdated += changes
}
}
return totalUpdated
}
// 条件付き削除:非アクティブユーザーとその投稿を削除
func cleanupInactiveUsers() throws -> (deletedUsers: Int, deletedPosts: Int) {
guard let db = db else { throw DatabaseError.connectionFailed }
var deletedUsers = 0
var deletedPosts = 0
try db.transaction {
// 非アクティブユーザーを取得
let inactiveUsers = users.filter(isActive == false)
// 非アクティブユーザーのIDを取得
var inactiveUserIds: [Int64] = []
for user in try db.prepare(inactiveUsers) {
inactiveUserIds.append(user[id])
}
// 関連する投稿を削除
for userIdValue in inactiveUserIds {
let userPosts = posts.filter(userId == userIdValue)
deletedPosts += try db.run(userPosts.delete())
}
// ユーザーを削除
deletedUsers = try db.run(inactiveUsers.delete())
}
return (deletedUsers: deletedUsers, deletedPosts: deletedPosts)
}
// データベースバックアップ(シンプル版)
func exportUserData() throws -> String {
guard let db = db else { throw DatabaseError.connectionFailed }
var exportData: [String: Any] = [:]
// ユーザーデータエクスポート
let allUsers = try getAllUsers()
let usersData = allUsers.map { user in
[
"id": user.id ?? 0,
"name": user.name,
"email": user.email,
"age": user.age,
"isActive": user.isActive,
"createdAt": ISO8601DateFormatter().string(from: user.createdAt),
"profile": user.profile?.toDictionary() ?? [:]
]
}
exportData["users"] = usersData
// 投稿データエクスポート
let allPosts = try db.prepare(posts)
let postsData = try allPosts.map { post in
[
"id": post[postId],
"userId": post[userId],
"title": post[title],
"content": post[content],
"published": post[published],
"publishedAt": post[publishedAt] != nil ? ISO8601DateFormatter().string(from: post[publishedAt]!) : nil,
"viewCount": post[viewCount]
]
}
exportData["posts"] = postsData
let jsonData = try JSONSerialization.data(withJSONObject: exportData, options: .prettyPrinted)
return String(data: jsonData, encoding: .utf8) ?? ""
}
// データベース統計
func getDatabaseStatistics() throws -> DatabaseStatistics {
guard let db = db else { throw DatabaseError.connectionFailed }
let totalUsers = try db.scalar(users.count)
let activeUsers = try db.scalar(users.filter(isActive == true).count)
let totalPosts = try db.scalar(posts.count)
let publishedPosts = try db.scalar(posts.filter(published == true).count)
let avgAge = try db.scalar(users.select(age.average)) ?? 0.0
let avgViewCount = try db.scalar(posts.select(viewCount.average)) ?? 0.0
return DatabaseStatistics(
totalUsers: totalUsers,
activeUsers: activeUsers,
totalPosts: totalPosts,
publishedPosts: publishedPosts,
averageUserAge: avgAge,
averagePostViewCount: avgViewCount
)
}
}
// 統計情報構造体
struct DatabaseStatistics {
let totalUsers: Int
let activeUsers: Int
let totalPosts: Int
let publishedPosts: Int
let averageUserAge: Double
let averagePostViewCount: Double
}
// 使用例
func demonstrateAdvancedFeatures() {
let database = UserDatabase()
do {
// ユーザーと初期投稿を同時作成
let profile = UserProfile(bio: "Content Creator", skills: ["Writing", "Photography"])
let user = User(name: "Content Creator", email: "[email protected]", age: 25, profile: profile)
let initialPosts = [
Post(userId: 0, title: "Welcome Post", content: "Welcome to my blog!"),
Post(userId: 0, title: "About Me", content: "Let me introduce myself..."),
Post(userId: 0, title: "My First Tutorial", content: "This is my first technical post.", published: true, publishedAt: Date())
]
let (userId, postIds) = try database.createUserWithPosts(user: user, initialPosts: initialPosts)
print("Created user \(userId) with posts: \(postIds)")
// 別のユーザー作成
let targetUser = User(name: "Another User", email: "[email protected]", age: 30)
let targetUserId = try database.createUser(targetUser)
// 投稿移譲
let transferCount = try database.transferPosts(fromUserId: userId, toUserId: targetUserId)
print("Transferred \(transferCount) posts from user \(userId) to user \(targetUserId)")
// バッチ年齢更新
let ageUpdates = [
(userId: userId, newAge: 26),
(userId: targetUserId, newAge: 31)
]
let updatedCount = try database.batchUpdateUserAges(userAgeUpdates: ageUpdates)
print("Updated ages for \(updatedCount) users")
// ユーザーを非アクティブに設定
_ = try database.setUserActive(userId, active: false)
// 非アクティブユーザーのクリーンアップ
let (deletedUsers, deletedPosts) = try database.cleanupInactiveUsers()
print("Cleanup: deleted \(deletedUsers) users and \(deletedPosts) posts")
// 統計情報
let stats = try database.getDatabaseStatistics()
print("\n=== Database Statistics ===")
print("Total users: \(stats.totalUsers)")
print("Active users: \(stats.activeUsers)")
print("Total posts: \(stats.totalPosts)")
print("Published posts: \(stats.publishedPosts)")
print("Average user age: \(String(format: "%.1f", stats.averageUserAge))")
print("Average post view count: \(String(format: "%.1f", stats.averagePostViewCount))")
// データエクスポート
let exportedData = try database.exportUserData()
print("\n=== Exported Data (first 200 chars) ===")
print(String(exportedData.prefix(200)) + "...")
} catch {
print("Error: \(error)")
}
}
エラーハンドリングとUtility
// カスタムエラー型
enum DatabaseError: Error, LocalizedError {
case connectionFailed
case userNotFound(Int64)
case postNotFound(Int64)
case invalidData(String)
case transactionFailed(String)
var errorDescription: String? {
switch self {
case .connectionFailed:
return "データベース接続に失敗しました"
case .userNotFound(let id):
return "ユーザーが見つかりません: ID \(id)"
case .postNotFound(let id):
return "投稿が見つかりません: ID \(id)"
case .invalidData(let message):
return "無効なデータ: \(message)"
case .transactionFailed(let message):
return "トランザクション失敗: \(message)"
}
}
}
// UserProfile用のUtility拡張
extension UserProfile {
func toJSONString() throws -> String {
let encoder = JSONEncoder()
let data = try encoder.encode(self)
return String(data: data, encoding: .utf8) ?? ""
}
static func fromJSONString(_ jsonString: String?) throws -> UserProfile? {
guard let jsonString = jsonString,
let data = jsonString.data(using: .utf8) else {
return nil
}
let decoder = JSONDecoder()
return try decoder.decode(UserProfile.self, from: data)
}
func toDictionary() -> [String: Any] {
return [
"bio": bio ?? "",
"website": website ?? "",
"location": location ?? "",
"skills": skills
]
}
}
// マイグレーション機能
extension UserDatabase {
// バージョン管理テーブル
private var migrationTable: Table { Table("migrations") }
private var versionColumn: Expression<Int> { Expression<Int>("version") }
private var appliedAtColumn: Expression<Date> { Expression<Date>("applied_at") }
// マイグレーション実行
func runMigrations() throws {
guard let db = db else { throw DatabaseError.connectionFailed }
// マイグレーションテーブル作成
try db.run(migrationTable.create(ifNotExists: true) { table in
table.column(versionColumn, primaryKey: true)
table.column(appliedAtColumn, defaultValue: Date())
})
let currentVersion = getCurrentMigrationVersion()
// 各マイグレーションを順次実行
for version in (currentVersion + 1)...getLatestMigrationVersion() {
try runMigration(version: version)
try recordMigration(version: version)
print("Applied migration version \(version)")
}
}
private func getCurrentMigrationVersion() -> Int {
do {
guard let db = db else { return 0 }
if let latestMigration = try db.pluck(migrationTable.order(versionColumn.desc)) {
return latestMigration[versionColumn]
}
} catch {
// マイグレーションテーブルが存在しない場合
}
return 0
}
private func getLatestMigrationVersion() -> Int {
return 3 // 現在の最新バージョン
}
private func runMigration(version: Int) throws {
guard let db = db else { throw DatabaseError.connectionFailed }
switch version {
case 1:
// ユーザーテーブルにlast_login_atカラム追加
try db.run("ALTER TABLE users ADD COLUMN last_login_at DATETIME")
case 2:
// 投稿テーブルにcategoryカラム追加
try db.run("ALTER TABLE posts ADD COLUMN category TEXT DEFAULT 'general'")
case 3:
// ユーザーテーブルにavatar_urlカラム追加
try db.run("ALTER TABLE users ADD COLUMN avatar_url TEXT")
default:
throw DatabaseError.invalidData("Unknown migration version: \(version)")
}
}
private func recordMigration(version: Int) throws {
guard let db = db else { throw DatabaseError.connectionFailed }
let insert = migrationTable.insert(
versionColumn <- version,
appliedAtColumn <- Date()
)
try db.run(insert)
}
}
// 非同期対応(iOS 15以降)
@available(iOS 15.0, *)
extension UserDatabase {
func createUserAsync(_ user: User) async throws -> Int64 {
return try await withUnsafeThrowingContinuation { continuation in
do {
let userId = try createUser(user)
continuation.resume(returning: userId)
} catch {
continuation.resume(throwing: error)
}
}
}
func getAllUsersAsync() async throws -> [User] {
return try await withUnsafeThrowingContinuation { continuation in
do {
let users = try getAllUsers()
continuation.resume(returning: users)
} catch {
continuation.resume(throwing: error)
}
}
}
}
// メイン実行例
func main() {
print("=== SQLite.swift Demo ===\n")
demonstrateBasicOperations()
print("\n" + String(repeating: "=", count: 50) + "\n")
demonstratePostOperations()
print("\n" + String(repeating: "=", count: 50) + "\n")
demonstrateAdvancedFeatures()
}
// 実行
main()