As you can see from the previous setup exercise and through testing, the Inventory service we imported already has the database wrapper attached. Let's review some of the script changes we imported and also get into details of the InventoryService
script:
CatchSceneController
located in the Assets/FoodyGo/Scripts/Controllers
folder in the Project window to open the script in the editor of your choice.CatchSceneController
is a new Start method calling our Inventory
service. Perform the following reviewing method:void Start() { var monster = InventoryService.Instance.CreateMonster(); print(monster); }
Start
method, the InventoryService
is being called as a singleton using the Instance
property. Then, the CreateMonster
method is called to generate a new monster
object. Finally, the monster
object is printed to the Console window with the print
method.Start
method is essentially just temporary test code we will move later. However, appreciate the ease of access the singleton pattern is providing us with.InventoryService
. Open the Monster
script located in the Assets/FoodyGo/Scripts/Database
folder. If you recall, we previously used the Monster
class to track our spawn's location within the MonsterService
. Instead, we decided to simplify the Monster
class to use just for inventory/database persistence and promote our older class to a new MonsterSpawnLocation
. The MonsterService
script was also updated to use the new MonsterSpawnLocation
.Monster
object that we will use to persist to the inventory/database:public class Monster { [PrimaryKey, AutoIncrement] public int Id { get; set; } public string Name { get; set; } public int Level { get; set; } public int Power { get; set; } public string Skills { get; set; } public double CaughtTimestamp { get; set; } public override string ToString() { return string.Format("Monster: {0}, Level: {1}, Power:{2}, Skills:{3}", Name, Level, Power, Skills); } }
Monster
attributes, which is a divergence from Unity and the more traditional C#. Next, notice that the top Id
property has a couple of attributes and PrimaryKey
and AutoIncrement
attached. If you are familiar with relational databases, you will understand this pattern right away.For those less familiar, all records/objects in our database need a unique identifier called a primary key. That identifier, in this case, called Id
, allows us to quickly locate an object later. The attribute AutoIncrement
allows us to know that the Id
property, an integer, will be automatically incremented when a new object is created. This alleviates us from managing the Id
of the objects ourselves and means the Id
property will be automatically set by the database.
ToString
method. Overriding ToString
allows us to customize the output of the object and is useful for debugging. Instead of having to inspect all the properties and print them to the console, we can instead simplify this to print(monster)
, as we saw in the CatchSceneController.Start
method earlier.InventoryService
script located in the Assets/FoodyGo/Scripts/Services
folder. As you can see, this class has a number of conditional sections in the Start
method in order to account for various deployment platforms. We won't review that code, but we will take a look at the last few lines of the Start
method:_connection = new SQLiteConnection(dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create); Debug.Log("Final PATH: " + dbPath); if (newDatabase) { CreateDB(); }else { CheckForUpgrade(); }
SQLiteConnection
, which creates a connection to the SQLite database. The connection is set by passing the database path (dbPath
) and options. The options provided to the connection are requesting read/write privileges and creating the database, if necessary. Therefore, if no existing database is found at the dbPath
, then a new empty database will be created. The next line just writes the database path to the Console.newDatabase
Boolean variable. The newDatabase
variable was previously set above the section of code by determining whether an existing database was already present. If newDatabase
is true, then we call CreateDB,
otherwise, we call CheckForUpgrade
.CreateDB
method does not create the physical database file on the device. That is instead done in the connection code we looked at earlier. The CreateDB
method instantiates the object tables or schema in the database as follows:private void CreateDB() { Debug.Log("Creating database..."); var minfo = _connection.GetTableInfo("Monster"); if(minfo.Count>0) _connection.DropTable<Monster>(); _connection.CreateTable<Monster>(); Debug.Log("Monster table created."); var vinfo = _connection.GetTableInfo("DatabaseVersion"); if(vinfo.Count>0) _connection.DropTable<DatabaseVersion>(); _connection.CreateTable<DatabaseVersion>(); Debug.Log("DatabaseVersion table created."); _connection.Insert(new DatabaseVersion { Version = DatabaseVersion }); Debug.Log("Database version updated to " + DatabaseVersion); Debug.Log("Database created."); }
Debug.Log
statements in this method; it is best to just think of them as helpful comments. After the install logging, we first determine whether the Monster
table has already been created using the GetTableInfo
method on the connection. GetTableInfo
returns the columns/properties of the table; if no columns or properties have been set, minfo
will have a count of 0
. If the table is present, however, we will delete or drop it and create a new table using our current Monster
properties.We follow the same pattern for the next table DatabaseVersion
. If GetTableInfo
returns vinfo.Count
> 0
, then delete the table, otherwise, just continue. You will see, as we add more objects to the InventoryService
, we will need to create the new tables in the same manner.
The SQLite4Unity3d wrapper provides us with an Object Relational Mapping (ORM) framework that allows us to map objects to relational database tables. This is why we will use the term object and table interchangeably at times. The following diagram shows how this mapping typically works:
An ORM example of monster to database
DatabaseVersion
object and store it in the database using the Insert
method on the _connection
. The DatabaseVersion
object is very simple and only has one property, called Version
. We use this object/table to track the version of the database.CheckForUpgrade
method, as follows:private void CheckForUpgrade() { try { var version = GetDatabaseVersion(); if (CheckDBVersion(version)) { //newer version upgrade required Debug.LogFormat("Database current version {0} - upgrading to {1}", version, DatabaseVersion); UpgradeDB(); Debug.Log("Database upgraded"); } } catch (Exception ex) { Debug.LogError("Failed to upgrade database, running CreateDB instead"); Debug.LogError("Error - " + ex.Message); CreateDB(); } }
CheckForUpgrade
method first gets the current database file version and then compares it to the version required by the code in the CheckDBVersion
method. If the code requires a newer database version, set by the DatabaseVersion
setting on the InventoryService
, then it upgrades the database. If the database doesn't require an upgrade, then the game uses the current database. However, if there is an error in the version check or some other error happens, then the code will assume something is wrong with the existing database and just create a new version. We will spend more time later on doing an actual database upgrade.CreateMonster
method called by the CatchSceneController
:public Monster CreateMonster() { var m = new Monster { Name = "Chef Child", Level = 2, Power = 10, Skills = "French" }; _connection.Insert(m); return m; }
CreateMonster
method currently just creates a hardcoded Monster
object and inserts it into the database using the _connection.Insert
method. It then returns the new object to the calling code. If you have experience working with relational databases and writing SQL code, hopefully, you can appreciate the simplicity of the Insert
here. We will update the CreateMonster
and other operation methods, in the next section of this chapter.13.58.82.79