0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[swift] sqliteベースでオブジェクトデータベースを作ってみた

Posted at

世間はiOSDC一色のようですが、辺境地でまったり独自で変な研究をしたのですが、一旦研究をフリーズさせる事にしたので、これまでの成果を記録に残す意味で、ここに記します。

最近、別件で技術的な難問に取り組んでいたのですが、その解決方法にオブジェクトデータベースを使えばいいのではないかと考え始め、試行錯誤した末に独自にオブジェクトデータベースを作ってみようと思い色々試していました。が、本来の技術的な難問は別の方法で解決でできるかもと思い始めたので、このプロジェクトを中断しようと思ったのですが、プロジェクト自体は面白そうなので、将来このプロジェクトに戻ってきた時の為の自分への記録とする事とします。

このプロジェクトが前進する毎に、なんか劣化版Realmを研究開発しているみたいで、モチベーションが下がります。が、テーマとしては面白いと考えているので、気分は持ちようです。

NSObjectはなんだかんだ言ってやはり優秀です。このNSObjectのアーカイバとSQLite3を活用すれば面白そうなオブジェクトデータベースができそうだと思いました。コードはここから取得可能です。

作戦

作戦はこうです。

  • データベースに生存するオブジェクトをNSObjectのサブクラスZObjectのサブクラスとして実装する事とします。
  • SQLite3をラップしたデータベース管理クラスZStorageを設計します。
  • ZObjectZStorageに登録時にIntegerPrimary Keyが採番されるものとします。この値をidentifierとし、一意にアクセスできるようにします。
  • あるオブジェクトが別のオブジェクトをencodeする時は、その別のオブジェクトのidentifierのみをencodeするものとする。
  • 別のオブジェクトがArrayのようなコレクションだとしても同様にidentifierのみをエンコードする
  • エンコードされたオブジェクトはデータベースにバイナリとして保存される
  • オブジェクトをインスタンス化する時は、identifierとアーカイブされたデータをNSKeyedUnarchiverで復元させます。
  • 復元時に別オブジェクトを復元させる必要がある場合でも、coderからidentifierのみを取得できれば、再起的に復元できます。

ZObject

ZObjectの実装を見てみる事とします。ZStorageidentifierがあれば、アンアーカイブされます。サブクラスされていても正確に復元できます。

.swift
open class ZObject: NSObject, NSCoding {
	private static let idKey = "id"
	public weak var storage: ZStorage?
	public var identifier: Int
	public init(storage: ZStorage) throws {
		self.identifier = -1
		self.storage = storage
		super.init()
		try storage.insert(object: self)
		assert(self.identifier != -1)
	}
	public init(identifier: Int, storage: ZStorage) throws {
		self.identifier = identifier
		self.storage = storage
		super.init()
	}
	required public init?(coder: NSCoder) {
		self.identifier = coder.decodeInteger(forKey: Self.idKey)
		self.storage = coder.storage!
		super.init()
	}
	deinit {
	}
	open func encode(with coder: NSCoder) {
		coder.encode(self.identifier, forKey: Self.idKey)
		self.storage = coder.storage!
	}
	open func save(storage: ZStorage?) throws {
		guard let storage = storage ?? self.storage else { throw ZStorageError.storageNotFound }
		try storage.save(object: self)
	}
	...
}

ZStorage

ZStorageを見てみます。Sqlite3のラッパーに独自のZSQLDatabaseを使っています。いやぁ、車輪の再発明は楽しいです。こんな感じで、オブジェクトのインスタンス化、保存、更新などを司る設計になっています。

.swift
open class ZStorage: NSObject {
	let database: ZSQLDatabase
	let fileURL: URL
	private static let objectTableKey = "object"
	private static let idKey = "id"
	private static let typeColumnKey = "type"
	private static let dataColumnKey = "data"
	private static let refcountKey = "refcount"
	public init(fileURL: URL) throws {
		typealias U = ZStorage
		self.fileURL = fileURL
		try self.database = ZSQLDatabase(fileURL: fileURL)
		let query = try self.database.query("""
			CREATE TABLE IF NOT EXISTS \(U.objectTableKey)(
				\(U.idKey) INTEGER PRIMARY KEY AUTOINCREMENT,
				\(U.typeColumnKey) TEXT NOT NULL,
				\(U.dataColumnKey) BINARY,
				\(U.refcountKey) INTEGER NOT NULL
			);
		""")
		let result = query.step()
		guard result.done else { throw ZSQLStatus(result) }
	}
	public func instanciateObject<T: ZObject>(identifier: Int, of objectType: T.Type) throws -> T? {
		return try self.instanciateObjects(identifiers: [identifier], of: T.self).first
	}
	public func instanciateObjects<T: ZObject>(identifiers: [Int], of objectType: T.Type) throws -> [T] {
		typealias U = ZStorage
		let typeString = String(describing: T.self)
		let identifiersList = identifiers.map { String($0) }.joined(separator: ",")
		let query = try self.database.query("""
			SELECT \(U.idKey), \(U.typeColumnKey), \(U.dataColumnKey) FROM \(U.objectTableKey)
			WHERE \(U.typeColumnKey) = '\(typeString)' AND \(U.idKey) IN (\(identifiersList)) AND \(U.refcountKey) > 0;
		""")
		var objects = [T]()
		while let row = query.step().row {
			guard let identifier: Int = row[U.idKey] as? Int else { fatalError() }
			if let object = self.objectCache.object(forKey: identifier as NSNumber) as? T  {
				objects.append(object)
			}
			else if let typeString = row[U.typeColumnKey] as? String, let data = row[U.dataColumnKey] as? Data {
				if let object = try? ZObjectKeyedUnarchiver.unarchiveTopLevelObjectWithData(data, storage: self) as? T {
					let objectType = String(describing: type(of: object))
					if objectType != typeString {
						print("\(Self.self): type mismatch, it is expected '\(typeString)' but '\(objectType)' id=\(identifier)")
					}
					objects.append(object)
					self.objectCache.setObject(object, forKey: identifier as NSNumber)
				}
				else { fatalError("\(Self.self): unable to unarchive an object id=\(identifier)") }
			}
			else { fatalError() }
		}
		return objects
	}
	public func insert<T: ZObject>(object: T) throws {
		typealias U = ZStorage
		let data = try! ZObjectKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false, storage: self)
		let typeString = String(describing: type(of: object))
		let refcount = 1
		let query = try self.database.query(
				"INSERT INTO \(U.objectTableKey) (\(U.typeColumnKey), \(U.dataColumnKey), \(U.refcountKey)) VALUES (?1, ?2, ?3);",
				typeString, data, refcount)
		let result = query.step()
		guard result.done else { throw ZSQLStatus(result)  }
		let identifier = self.database.lastInsertRowID
		object.identifier = Int(identifier)
		object.storage = self
		self.objectCache.setObject(object, forKey: identifier as NSNumber)
	}
	public func save<T: ZObject>(object: T) throws {
		if object.storage == self { try self.update(object: object) }
		else { try self.insert(object: object) }
	}
	public func update<T: ZObject>(object: T) throws {
		typealias U = ZStorage
		assert(object.storage == self)
		guard object.identifier > 0 else { return }
		let identifier = object.identifier
		let data = try! ZObjectKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false, storage: self)
		let typeString = String(describing: type(of: object))
		let query = try self.database.query(
			"UPDATE \(U.objectTableKey) SET \(U.dataColumnKey) = ?1, \(U.typeColumnKey) = ?2  WHERE \(U.idKey) = \(identifier);",
			data, typeString)
		let result = query.step()
		guard result.done else { throw ZSQLStatus(result) }
	}
	...
}

ZObjectはデータベースと常に結び付けられている必要があるので、UnarchiveされたZObjectZStorageと紐づいている必要があるため。カスタム化したNSKeyedArchiverKeyedUnarchiverを使って、ZObjectがインスタンス化された瞬間にweak var storage: ZStorage?にstorageを設定するとともに、ZStorageは同オブジェクトを二重にインスタンス化させないためキャッシュします。

ZObjectKeyedArchiver, ZObjectKeyedUnarchiver

ZObjectはアーカイブ、アンアーカイブの際に常にZStorageにアクセスする必要があります。例えばrequired init?(coder: NSCoder)時に別オブジェクトをインスタンス化させるために、ZStorageが必要となりますが、NSCoderはその事を知らないので、NSKeyedUnarchiverNSKeyedArchiverのサブクラスを用意し、さらに NSCoderのプロトコルエクステンションを用意して、NSCoderはいつでもどのZStorageからZObjectをインスタンス化しているか知ることができます。

.swift
public class ZObjectKeyedArchiver: NSKeyedArchiver {
	
	private static let semaphore = DispatchSemaphore(value: 1)
	private static var storage: ZStorage?
	private (set) public var _storage: ZStorage?

	class func archivedData(withRootObject object: Any, requiringSecureCoding requiresSecureCoding: Bool, storage: ZStorage) throws -> Data {
		Self.semaphore.wait()
		Self.storage = storage
		return try super.archivedData(withRootObject: object, requiringSecureCoding: requiresSecureCoding)
	}
	override init() {
		self._storage = Self.storage
		super.init()
		Self.semaphore.signal()
	}
	override init(requiringSecureCoding requiresSecureCoding: Bool) {
		self._storage = Self.storage
		super.init(requiringSecureCoding: requiresSecureCoding)
	}
}

public class ZObjectKeyedUnarchiver: NSKeyedUnarchiver {
	
	private static let semaphore = DispatchSemaphore(value: 1)
	private static var storage: ZStorage?
	private (set) public var _storage: ZStorage?

	class func unarchiveTopLevelObjectWithData(_ data: Data, storage: ZStorage) throws -> Any? {
		Self.semaphore.wait()
		Self.storage = storage
		return try super.unarchiveTopLevelObjectWithData(data)
	}
	override init(forReadingWith data: Data) {
		self._storage = Self.storage
		super.init(forReadingWith: data)
		Self.storage = nil
		Self.semaphore.signal()
	}
	override init(forReadingFrom data: Data) throws {
		self._storage = Self.storage
		try super.init(forReadingFrom: data)
		self.decodingFailurePolicy = .setErrorAndReturn
		Self.storage = nil
		Self.semaphore.signal()
	}
}

public extension NSCoder {
	var storage: ZStorage? {
		if let archiver = self as? ZObjectKeyedArchiver {
			return archiver._storage
		}
		if let unarchiver = self as? ZObjectKeyedUnarchiver {
			return unarchiver._storage
		}
		return nil
	}
}

この記事を書いている時には失念してしまいましたが、NSKeyedUnarchiverだったか、NSKeyedArchiverだったかは、init()で一旦インスタンス化して、archive()したデータはunarchive()で元のオブジェクトが取り出せないので、classメソッドをオーバーライドして実装しています。さらにinit(requiringSecureCoding requiresSecureCoding: Bool)init()両方が、ストアドプロパティを初期化せよとうるさいので、こんな変なコーディングになっています。じゃあ、init()を削るとそれもコンパイラに怒られます。おそらく、クラスタクラスか何かの呪いのような気がしますが、深入りしない事としました。

実際の使用例

しょぼいお絵描きアプリを想定してみます。グラフィックオブジェクトは2種類CircleRectangleの二種類ですが、共通のプロトコルShapeに準拠していルものとします。グラフィックオブジェクトはレイヤー上に配置され、レイヤーはページ内に、さらにページはコンテンツに存在するものとします。それぞれが、ZObjectのサブクラスでデータベース上に永続化されるものとするとこんな感じでコーディングします。

.swift
protocol Shape: ZObject {
}

class Circle: ZObject, Shape {
	var center: CGPoint
	var radius: CGFloat
	enum Keys: String { case center, radius }
	init(center: CGPoint, radius: CGFloat, storage: ZStorage) throws {
		self.center = center
		self.radius = radius
		try super.init(storage: storage)
	}
	override func encode(with coder: NSCoder) {
		super.encode(with: coder)
		coder.encode(self.center, forKey: Self.Keys.center.rawValue)
		coder.encode(self.radius, forKey: Self.Keys.radius.rawValue)
	}
	required init?(coder: NSCoder) {
		self.center = coder.decodeCGPoint(forKey: Self.Keys.center.rawValue)
		self.radius = coder.decodeObject(forKey: Self.Keys.radius.rawValue) as! CGFloat
		super.init(coder: coder)
	}
	override var description: String {
		return "{\(Self.self): center=\(self.center), radius=\(radius)}"
	}
}

class Rectangle: ZObject, Shape {
	var origin: CGPoint
	var size: CGSize
	enum Keys: String { case origin, size }
	init(origin: CGPoint, size: CGSize, storage: ZStorage) throws {
		self.origin = origin
		self.size = size
		try super.init(storage: storage)
	}
	override func encode(with coder: NSCoder) {
		super.encode(with: coder)
		coder.encode(self.origin, forKey: Self.Keys.origin.rawValue)
		coder.encode(self.size, forKey: Self.Keys.size.rawValue)
	}
	required init?(coder: NSCoder) {
		self.origin = coder.decodeCGPoint(forKey: Self.Keys.origin.rawValue)
		self.size = coder.decodeCGSize(forKey: Self.Keys.size.rawValue)
		super.init(coder: coder)
	}
	override var description: String {
		return "{\(Self.self): origin=\(self.origin), size=\(size)}"
	}
}

class Layer: ZObject {
	var shapes: [Shape]
	static let shapesKey = "shapes"
	init(shapes: [Shape], storage: ZStorage) throws {
		self.shapes = shapes
		try super.init(storage: storage)
	}
	override func encode(with coder: NSCoder) {
		coder.encode(shapes, forKey: Self.shapesKey)
		super.encode(with: coder)
	}
	required init?(coder: NSCoder) {
		self.shapes = coder.decodeObject(forKey: Self.shapesKey) as! [Shape]
		super.init(coder: coder)
	}
}

class Page: ZObject {
	var layers: [Layer]
	static let layersKey = "layers"
	init(layers: [Layer], storage: ZStorage) throws {
		self.layers = layers
		try super.init(storage: storage)
	}
	override func encode(with coder: NSCoder) {
		coder.encode(layers, forKey: Self.layersKey)
		super.encode(with: coder)
	}
	required init?(coder: NSCoder) {
		self.layers = coder.decodeObject(forKey: Self.layersKey) as! [Layer]
		super.init(coder: coder)
	}
}

class Contents: ZObject {
	var pages: [Page]
	static let pagesKey = "pages"
	init(pages: [Page], storage: ZStorage) throws {
		self.pages = pages
		try super.init(storage: storage)
	}
	override func encode(with coder: NSCoder) {
		coder.encode(pages, forKey: Self.pagesKey)
		super.encode(with: coder)
	}
	required init?(coder: NSCoder) {
		self.pages = coder.decodeObject(forKey: Self.pagesKey) as! [Page]
		super.init(coder: coder)
	}
}

Contentsクラスは、Pageを、PageLayerを、LayerShapeを内包する事ができます。コードでモデルを生成する例を以下に示します。

.swift
do {
	let storage = try ZStorage(fileURL: self.file1URL)
	let c1 = try Circle(center: CGPoint(x: 100, y: 200), radius: 300, storage: storage)
	let c2 = try Circle(center: CGPoint(x: 400, y: 500), radius: 600, storage: storage)
	let c3 = try Circle(center: CGPoint(x: 700, y: 800), radius: 900, storage: storage)
	let r1 = try Rectangle(origin: CGPoint(x: 101, y: 102), size: CGSize(width: 103, height: 104), storage: storage)
	let r2 = try Rectangle(origin: CGPoint(x: 201, y: 202), size: CGSize(width: 203, height: 204), storage: storage)
	let r3 = try Rectangle(origin: CGPoint(x: 301, y: 302), size: CGSize(width: 303, height: 304), storage: storage)
	let layer1 = try Layer(shapes: [c1, r1], storage: storage)
	let layer2 = try Layer(shapes: [c2, r2], storage: storage)
	let layer3 = try Layer(shapes: [c3, r3], storage: storage)
	let page1 = try Page(layers: [layer1, layer2], storage: storage)
	let page2 = try Page(layers: [layer3], storage: storage)
	let contents = try Contents(pages: [page1, page2], storage: storage)
	try contents.save(storage: storage)
	print(contents.identifier)
} catch { ... }

ZStorageにオブジェクトを登録した時にidentifierが決定するのでそのidUserDefaultなりなんなり保存しておきます。それを後に、または次回に以下のようにオブジェクトグラフ全体をロードする事ができます。Arrayもです。ここのオブジェクトは常にidentifierからアンアーカイブされています。Shapeのようなプロトコルで抽象化されたオブジェクトでもちゃんとタイプ情報が損失することなく復元できます。NSObjectNSCodingすごいです。

.swift
do {
	let identifier = ...
	if let contents = try storage.instanciateObject(identifier: identifier, of: Contents.self) {
		...
	}
} catch { ... }

特定のidentifierを基にではなく、特定のクラスのインスタンスを一括で取得する事もできます。

.swift
do {
	let storage = ...
	let identifier = ...
	if let pages = try storage.instanciateObject(of: Page.self) {
		for page in pages {
			...
		}
	}
} catch { ... }

ZSQL

再発明した車輪のコードも紹介します。最初は生のsqliteを叩いていましたが、statementの扱いが非常に面倒くさいと思ったので、独自にラッパーを書きました。

.swift
import Foundation
import SQLite3

public struct ZSQLStatus: Error, CustomStringConvertible {
	let status: Int32
	public init(_ status: Int32) {
		self.status = status
	}
	public init(_ result: ZSQLResult) {
		self.status = result.status
	}
	public var description: String {
		switch self.status {
		case SQLITE_OK: return "SQLITE_OK: Successful result"
		case SQLITE_ERROR: return "SQLITE_ERROR: Generic error"
		case SQLITE_INTERNAL: return "SQLITE_INTERNAL: Internal logic error in SQLite"
		case SQLITE_PERM: return "SQLITE_PERM: Access permission denied"
		case SQLITE_ABORT: return "SQLITE_ABORT: Callback routine requested an abort"
		case SQLITE_BUSY: return "SQLITE_BUSY: The database file is locked"
		case SQLITE_LOCKED: return "SQLITE_LOCKED: A table in the database is locked"
		case SQLITE_NOMEM: return "SQLITE_NOMEM: A malloc() failed"
		case SQLITE_READONLY: return "SQLITE_READONLY: Attempt to write a readonly database"
		case SQLITE_INTERRUPT: return "SQLITE_INTERRUPT: Operation terminated by sqlite3_interrupt()"
		case SQLITE_IOERR: return "SQLITE_INTERRUPT: Some kind of disk I/O error occurred"
		case SQLITE_CORRUPT: return "SQLITE_CORRUPT: The database disk image is malformed"
		case SQLITE_NOTFOUND: return "SQLITE_NOTFOUND: Unknown opcode in sqlite3_file_control()"
		case SQLITE_FULL: return "SQLITE_FULL: Insertion failed because database is full"
		case SQLITE_CANTOPEN: return "SQLITE_CANTOPEN: Unable to open the database file"
		case SQLITE_PROTOCOL: return "SQLITE_PROTOCOL: Database lock protocol error"
		case SQLITE_EMPTY: return "SQLITE_EMPTY: Internal use only"
		case SQLITE_SCHEMA: return "SQLITE_SCHEMA: The database schema changed"
		case SQLITE_TOOBIG: return "SQLITE_TOOBIG: String or BLOB exceeds size limit"
		case SQLITE_CONSTRAINT: return "SQLITE_CONSTRAINT: Abort due to constraint violation"
		case SQLITE_MISMATCH: return "SQLITE_MISMATCH: Data type mismatch"
		case SQLITE_MISUSE: return "SQLITE_MISUSE: Library used incorrectly"
		case SQLITE_NOLFS: return "SQLITE_NOLFS: Uses OS features not supported on host"
		case SQLITE_AUTH: return "SQLITE_AUTH: Authorization denied"
		case SQLITE_FORMAT: return "SQLITE_FORMAT: Not used"
		case SQLITE_RANGE: return "SQLITE_RANGE: 2nd parameter to sqlite3_bind out of range"
		case SQLITE_NOTADB: return "SQLITE_NOTADB: File opened that is not a database file"
		case SQLITE_NOTICE: return "SQLITE_NOTICE: Notifications from sqlite3_log()"
		case SQLITE_WARNING: return "SQLITE_WARNING: Warnings from sqlite3_log()"
		case SQLITE_ROW: return "SQLITE_ROW: sqlite3_step() has another row ready"
		case SQLITE_DONE: return "SQLITE_DONE: sqlite3_step() has finished executing"
		default: return "Unknown SQLITE status \(self.status)"
		}
	}
}

enum ZSQLError: Error {
	case failedOpenFile
	case columnNotFound
}

public class ZSQLDatabase {
	let fileURL: URL
	private (set) var database: OpaquePointer?
	private (set) var status: Int32
	public init(fileURL: URL) throws {
		self.fileURL = fileURL
		self.status = sqlite3_open(fileURL.path, &self.database)
		guard self.status == SQLITE_OK else { fatalError(self.errorMessage ?? ZSQLStatus(self.status).description) }
	}
	public var errorMessage: String? { String(utf8String: sqlite3_errmsg(self.database)) }
	public func query(_ query: String, _ arguments: ZSQLColumnValue...) throws -> ZSQLQuery {
		return try ZSQLQuery(database: self, query: query, arguments: arguments)
	}
	public var lastInsertRowID: Int {
		return Int(sqlite3_last_insert_rowid(self.database))
	}
	public var autocommit: Bool {
		return sqlite3_get_autocommit(self.database) != 0
	}
	public var underTransaction: Bool {
		return !self.autocommit
	}
}

public class ZSQLQuery {
	let database: ZSQLDatabase
	let query: String
	private (set) var statement: OpaquePointer?
	private (set) var status: Int32
	init(database: ZSQLDatabase, query: String, arguments: [ZSQLColumnValue]) throws {
		self.database = database
		self.query = query
		self.status = sqlite3_prepare_v2(self.database.database, query, -1, &self.statement, nil)
		guard self.status == SQLITE_OK else { throw ZSQLStatus(self.status) }
		
		for (index, argment) in arguments.enumerated() {
			let index = Int32(index + 1)
			switch argment {
			case let value as Int:
				sqlite3_bind_int64(self.statement, index, sqlite3_int64(value))
			case let value as Double:
				sqlite3_bind_double(self.statement, index, value)
			case let value as String:
				sqlite3_bind_text(self.statement, index, (value as NSString).utf8String, -1, nil)
			case let value as Data:
				sqlite3_bind_blob(self.statement, index, (value as NSData).bytes, Int32(value.count), nil)
			case is NSNull:
				sqlite3_bind_null(self.statement, index)
			default:
				break
			}
		}
	}
	deinit {
		sqlite3_finalize(statement)
	}
	func step() -> ZSQLResult {
		self.status = sqlite3_step(statement)
		let result = ZSQLResult(status: self.status, statement: self.statement, query: self)
		self.result = result
		return result
	}
	var result: ZSQLResult?
}

public protocol ZSQLColumnValue {}
extension Int: ZSQLColumnValue {}
extension Double: ZSQLColumnValue {}
extension String: ZSQLColumnValue {}
extension Data: ZSQLColumnValue {}
extension NSNull: ZSQLColumnValue {}

public class ZSQLResult {
	let status: Int32
	let statement: OpaquePointer?
	let query: ZSQLQuery
	init(status: Int32, statement: OpaquePointer?, query: ZSQLQuery) {
		self.status = status
		self.statement = statement
		self.query = query
	}
	var row: ZSQLRow? {
		return (self.status == SQLITE_ROW) ? ZSQLRow(statement: self.statement) : nil
	}
	var OK: Bool { self.status == SQLITE_OK }
	var done: Bool { self.status == SQLITE_DONE }
}

public class ZSQLRow {
	let statement: OpaquePointer?
	public init(statement: OpaquePointer?) {
		self.statement = statement
	}
	public func rawValue(column: Int) -> ZSQLColumnValue? {
		let count = sqlite3_column_count(self.statement)
		guard column >= 0 && column < count else { fatalError() }
		switch sqlite3_column_type(self.statement, Int32(column)) {
		case SQLITE_INTEGER:
			let value = sqlite3_column_int64(self.statement, Int32(column))
			return Int(value)
		case SQLITE_FLOAT:
			let value = sqlite3_column_double(self.statement, Int32(column))
			return Double(value)
		case SQLITE_BLOB:
			let length = Int(sqlite3_column_bytes(self.statement, Int32(column)))
			let data = Data(bytes: sqlite3_column_blob(self.statement, Int32(column)), count: length)
			return data
		case SQLITE_TEXT:
			let string = String(cString: sqlite3_column_text(self.statement, Int32(column))) // type in string
			return string
		case SQLITE_NULL:
			return NSNull()
		default:
			return nil
		}
	}
	public func value<T: ZSQLColumnValue>(index: Int) -> T? {
		return self.rawValue(column: index) as? T
	}
	public func value<T: ZSQLColumnValue>(named name: String) -> T? {
		guard let index = self.columnNames.firstIndex(of: name) else { fatalError() }
		return self.rawValue(column: index) as? T
	}
	public lazy var columnNames: [String] = {
		return (0 ..< sqlite3_column_count(self.statement)).map { index in
			guard let rawValue = sqlite3_column_name(self.statement, index) else { fatalError() }
			return String(cString: rawValue)
		}
	}()
	public lazy var values: [String: ZSQLColumnValue] = {
		return (0 ..< sqlite3_column_count(self.statement)).reduce(into: [String: ZSQLColumnValue]()) { dictionary, index in
			guard let rawValue = sqlite3_column_name(self.statement, index) else { fatalError() }
			let columnName = String(cString: rawValue)
			dictionary[columnName] = self.rawValue(column: Int(index))
		}
	}()
	subscript(key: String) -> ZSQLColumnValue? {
		return self.values[key]
	}
}

最後に

なんか、面白そうと思ったあなたでも、決して遊び以上のプロジェクトではしようしないでください。著者の思いつきや思い込みが満載で、テストも十分にされていません。アイディアの検証程度に書いているコードなので全く責任は取れません。しかし、それでも興味があるひとは、ひとまず遊びで触ってみてください。

また、いつかこのプロジェクトに戻ってくる時があれば、CoreData のオルタナティブくらいの立ち位置ができれは面白そうです。Core Data と違いオブジェクト間のリレーションをスキーマにするのではないので、より複雑なモデルが簡単に実装できそうですが、マイグレーションなどを考えるとなかなか簡単にはいかないようにも思います。

環境

執筆時点での環境は以下の通りです。

macOS Big Sur 11.5.2 (Apple M1)
Xcode Version 13.0 beta 5 (13A5212g)
Apple Swift version 5.5
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?