🪶

Godot で SQLite を使うメモ

2024/12/29に公開

addons

godot-sqlite
https://github.com/2shady4u/godot-sqlite

インストール

AssetLib 経由でDL可能
AssetLib

基本的な使い方

# 接続
var conn : SQLite
conn = SQLite.new()
conn.path = "res://path/to/data"
conn.verbosity_level = SQLite.VERBOSE
conn.foreign_keys = true
conn.open_db()

# テーブル作成
conn.create_table('users', {
    "id": {"data_type":"int", "primary_key": true, "auto_increment": true, "not_null": true},
    "name": {"data_type":"text", "not_null": true}
})

# データ作成
conn.insert_row('users', { "name": "Foo" })

# 検索
var id = conn.get_last_insert_rowid()
conn.select_rows('users', 'id = %s' % [id], ['*'])

# 更新
conn.update_rows('users', 'id = %s' % [id], { "name": "Bar" })

# 削除
conn.delete_rows('users', 'id = %s' % [id])

もう少し扱いやすくする

Rails の ActiveRecord に慣れているので、インタフェースを ActiveRecord ライクにするラッパーを作成しました。

欲しいのは、DB.Post.create({ "title": "Test" }) のように書いたらDBへ登録してくれるインタフェースです。

以下のようなフォルダ構成でファイルを作成します。

tree
src/
├── Main.gd
├── Main.tscn
└── db
    ├── base
    │   ├── record.gd
    │   └── table.gd
    ├── db.gd
    └── models
        ├── comment.gd
        └── post.gd

src/db/db.gdDB として autoloads に指定。

src/db/db.gd
extends Node
# class_name DB defined in autoloads

var path := "res://data/sqlite-example"
const verbosity_level : int = SQLite.VERBOSE
var conn : SQLite

# Model
const _PostModel  = preload("res://src/db/models/post.gd")
const _CommentModel = preload("res://src/db/models/comment.gd")
@onready var Post    = _PostModel.new()
@onready var Comment = _CommentModel.new()

func _ready() -> void:
	open()

func open(_path: String = path) -> void:
	conn = SQLite.new()
	conn.path = _path
	conn.verbosity_level = verbosity_level
	conn.foreign_keys = true
	conn.open_db()

func close() -> void:
	conn.close_db()

src/db/base/table.gd で ActiveRecord でいうクラスメソッド、src/db/base/record.gd でインスタンスメソッドを定義します。
これが各モデルの基底クラスになるので共通のインタフェースはここに定義します。
godot-sqlite で SELECT 句を発行して取得できる結果は Dictionary型なので、以下で定義するrecordize_by関数を各モデルから呼んでそのクラスで自動的に結果をラップします(※後述)

src/db/base/table.gd
class_name DB_Table
class Record extends DB_Record: pass

func table_name() -> String: # override this method
	return ''

func schema() -> Dictionary: # override this method
	var dict = Dictionary()
	return dict

func recreate_table() -> void:
	DB.conn.drop_table(table_name())
	DB.conn.create_table(table_name(), schema())

func all() -> Array:
	return where('')

func where(condition: String) -> Array:
	return DB.conn.select_rows(table_name(), condition, ['*']).map(
		func(params): return recordize(params))

func recordize(params: Dictionary):
	return params # override this method

func recordize_by(klass, params: Dictionary) -> Variant:
	var record = klass.new()
	record.model = self
	record.new_record = not params.has('id')
	record.assign_attributes(params)
	return record
# 略
src/db/base/record.gd
class_name DB_Record

var model : DB_Table
var new_record : bool = false

func column_names() -> Array[String]:
	return [] # override this method

func identify_column() -> String:
	return 'id'

func identify_value():
	return get(identify_column())

func identify_condition() -> String:
	var condition_fields = [identify_column(), identify_value()]
	if not condition_fields.all(func(c): return c): return ''
	return "%s = %s" % condition_fields

func create() -> void:
	var dict = attributes()
	if not identify_value(): dict.erase(identify_column())
	var row_id = model.create(dict)
	set(identify_column(), row_id)
	new_record = false

# 略

あとは src/db/models/xxx.gd で各テーブルに関する定義を記述します。

src/db/models/post.gd
extends DB_Table
func table_name() -> String: return 'posts'

func recordize(json: Dictionary): return recordize_by(Record, json)
class Record extends DB_Record:
	var id : int
	var title : String
	func column_names(): return ['id', 'title']

func schema() -> Dictionary:
	var dict = Dictionary()
	dict["id"] = {"data_type":"int", "primary_key": true, "auto_increment": true, "not_null": true}
	dict["title"] = {"data_type":"text", "not_null": true}
	return dict

テストコード

上記のようにラッパーを書くと以下のように呼び出せるようになります。(※↑の記述で略した関数もあります)

test/db/models/test_post.gd
extends GutTest

func before_each():
	DB.reopen("res://data/sqlite-example.test")
	DB.recreate_tables()

func test_count_if_nothing():
	assert_eq(DB.Post.count(), 0)

func test_create():
	var record = DB.Post.create({ "title": "foo" })
	assert_eq(DB.Post.count(), 1)
	assert_eq(record.attributes()['title'], 'foo')
	assert_eq(record.title, 'foo')
	record = DB.Post.first()
	assert_eq(record.attributes()['title'], 'foo')

func test_where():
	DB.Post.create({ "title": "foo" })
	assert_eq(DB.Post.where("title = 'bar'"), [])
	assert_eq(DB.Post.where("title = 'foo'").size(), 1)
	assert_eq(DB.Post.where("title = 'foo'")[0].title, 'foo')

func test_first_if_nothing():
	assert_eq(DB.Post.first(), null)

func test_first_if_one():
	DB.Post.create({ "title": "foo" })
	assert_eq(DB.Post.first().title, 'foo')

func test_update():
	var record = DB.Post.create({ "title": "test" })
	record.update({ "title": "bar" })
	assert_eq(record.title, 'bar')
	record = DB.Post.first()
	assert_eq(record.title, 'bar')

func test_save():
	var record = DB.Post.create({ "title": "test" })
	record.title = 'bar'
	record.save()
	assert_eq(record.title, 'bar')
	record = DB.Post.first()
	assert_eq(record.title, 'bar')

func test_destroy():
	var record = DB.Post.create({ "title": "test" })
	assert_eq(DB.Post.count(), 1)
	record.destroy()
	assert_eq(DB.Post.count(), 0)

func test_delete_all():
	DB.Post.create({ "title": "test" })
	assert_eq(DB.Post.count(), 1)
	DB.Post.delete_all()
	assert_eq(DB.Post.count(), 0)

# 略 

できたもの

以下のリポジトリに置いています。
https://github.com/tkmfujise/godot_sqlite_example

あくまで自分用ですが、今後も少しずつ使いやすいよういろいろ足していこうと思います。
src/db/base ディレクトリをそのまま持って行って、src/db/models/xxx.gd を定義すれば、
あとは godot-sqlite アドオンを入れれば流用できると思います。

Discussion