🪶
Godot で SQLite を使うメモ
addons
godot-sqlite
インストール
AssetLib 経由でDL可能
基本的な使い方
# 接続
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.gd
は DB
として 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)
# 略
できたもの
以下のリポジトリに置いています。
あくまで自分用ですが、今後も少しずつ使いやすいよういろいろ足していこうと思います。
src/db/base
ディレクトリをそのまま持って行って、src/db/models/xxx.gd
を定義すれば、
あとは godot-sqlite アドオンを入れれば流用できると思います。
Discussion