1、前言
在unity中构建项目连接数据库时,如果只是连接一个数据库实现基本的增删改查功能,我们可以选择SQLite、MySql或者SqlServer,导入相应的dll文件放到Assets文件下的Plugins文件中,编写相应的脚本即可对数据库进行连接以及增删改查等操作。如果要求同时支持多种数据库连接,就有所不同。
一开始我在github上使用了FreeSql、Dapper等库在Unity中去实现多数据库连接时,在编辑器中均能正常运行,但是打包时遇到的问题都是如出一辙:打包(这里其测试的是Windows)时会报错,导致打包的exe文件根本无法运行,提示"Failed to load IL2CPP"(使用IL2CPP静态编译),大致类似错误如下:
Error: il2cpp.InvalidCommandLineArgumentsException: One or more assemblies must be specified using either --directory or --assembly at il2cpp.Conversion.ContextDataFactory.CreateConversionDataFromOptions(Il2CppCommandLineArguments il2CppCommandLineArguments)
或者连接器相关报错:
Library\Bee\artifacts\unitylinker_dwek.traceevents failed with output:......
测试了几个比较流行的.NET的数据库都在打包时编译报错,只能使用其他方法去实现:使用工厂方法设计模式来实现每个数据库的连接和相关操作(可添加数据库),传入一个数据库类型枚举参数实现数据库的连接切换。
本文使用的Unity及相关dll文件如下:
- Unity版本:2021.3.16f1
- 代码编辑器:Visual Studio 2022
- MySql数据库:MySqlConnector.dll,版本2.0.0.0
- Sqlite数据库:sqlite3.dll,版本3.19.3.0
- SqlServer数据库:System.Data.SqlClient.dll,版本4.1.1.0
- Scripting Backend:IL2CPP
- Api Compatibility Level:.NET Framework
Sqlite使用的是github上的SQLite4Unity3d库,加入sqlite3.dll的同时需要引入脚本SQLite.cs文件,相关资源及使用参见github地址:github.com/robertohuer...
关于SqlServer,添加System.Data.SqlClient.dll后,打包没有报错,但是运行连接sqlServer数据库没有响应,通过查看日志,出现报错:
NotSupportedException: Encoding 936 data could not be found.
网上搜索了下,因为I18N.dll和I18N.CJK.dll文件引起的报错,I18N.dll是一个国际化库,用于处理通用的文本国际化任务,而I18N.CJK.dll是专门用于支持亚洲语言(CJK,中文、日文、韩文)的国际化库。
解决方法是从Unity编辑器的目录去寻找这两个dll文件,并放到Asetts文件夹下的Plugins文件里面,我尝试了下,从编辑器里面找到的这两个dll文件无法编译,出现报错:
Loading assembly failed: "Assets/Plugins/Database/I18N.dll" reason: File does not contain a valid CIL image
最后,我在网上下载了最新的这两个dll文件,放到Asetts文件夹下的Plugins文件里面,重新编译打包,能正常连接SqlServer数据库,对应的版本如下:
- I18N.dll,版本2.0.5.0,
- I18N.CJK.dll,版本2.0.5.0
本文数据库连接的主要的UML类图如下:
2、数据库连接
2.1数据库表数据结构定义
如果在数据库连接后需要自动创建表,我们就需要先定义表的数据结构,连接数据库后我们想对相应的表进行操作,也需要建立对应的表数据结构。比如该例子:
c#
/// <summary>
/// The database table struct.
/// </summary>
public class DatabaseTableStruct
{
/// <summary>
/// Gets or sets 编号
/// </summary>
public int id { get; set; }
/// <summary>
/// Gets or sets工件型号
/// </summary>
public string specimenType { get; set; }
/// <summary>
/// Gets or sets检测时间
/// </summary>
public DateTime dateTime { get; set; }
/// <summary>
/// Gets or sets检测人员
/// </summary>
public string manipulator { get; set; }
/// <summary>
/// Gets or sets检测结果
/// </summary>
public string result { get; set; }
}
另外,因为后续用SQL语句进行增删改查,需要用到表的列名,为了方便管理,定义一个常数类(与表数据一一对应)进行使用,在数据库工厂的构造函数中进行传值给到数据库工厂使用。
c#
/// <summary>
/// The table data const.
/// </summary>
internal class TableDataConst
{
/// <summary>
/// id
/// </summary>
public const string kId = "id";
/// <summary>
/// 工件型号
/// </summary>
public const string kSpecimenType = "specimenType";
/// <summary>
/// 检测日期
/// </summary>
public const string kDateTime = "dateTime";
/// <summary>
/// 操作员
/// </summary>
public const string kManipulator = "manipulator";
/// <summary>
/// 检测结果
/// </summary>
public const string kResult = "result";
}
常数类在构造器中传值给到数据库工厂类使用,当然也可以在数据库工厂中直接进行定义,不需要创建单独的常数类。
c#
/// <summary>
/// Initializes a new instance of the <see cref="DatabaseFactoryBase"/> class.
/// </summary>
public DatabaseFactoryBase()
{
id = TableDataConst.kId;
specimenType = TableDataConst.kSpecimenType;
qrCode = TableDataConst.kQrCode;
dateTime = TableDataConst.kDateTime;
manipulator = TableDataConst.kManipulator;
result = TableDataConst.kResult;
dataPath = TableDataConst.kDataPath;
}
2.1数据库管理器初始化
我们使用一个单例类DatabaseManager来管理数据的连接以及增删改查等操作,连接配置类DatabaseConnectionConfig来管理数据库的连接配置(密码、数据库名称、端口等)。
项目启动时,我们初始化数据库连接的相关配置,主要包括数据库工厂基类实例的创建、数据库db文件路径初始化、数据库连接配置文件初始化(读取)、默认数据库的连接(通过序列化来保存该值)等。初始化代码如下:
c#
/// <summary>
/// 初始化
/// </summary>
public void Init()
{
m_Factory = new DatabaseFactoryBase();
InitPath();
InitEvent();
ReadSqlConfig();
var databaseType = (DataBaseType)GetDatabaseType();
m_Database = m_Factory.CreateDbObject(databaseType);
m_Database.CreateDbConnection(m_DatabaseFilePath, m_ConnectionConfig);
}
2.2连接字符串
MySql数据库的连接字符串如下:
c#
var connectionString = $"Server={connectionConfig.ip};Port={connectionConfig.port};Database={databaseName};User Id={connectionConfig.id};Password={connectionConfig.password};";
Sqlite数据库的连接字符串比较简单,如下:
c#
var connectionString = $"{databasePath}\\{databaseName}.db";
SqlServer数据库的连接字符串如下:
c#
var connectionString = $"Data Source = {connectionConfig.ip}; Initial Catalog = {databaseName}; "+ $"User Id = {connectionConfig.id}; Password = {connectionConfig.password}; ";
上述连接字符串中,sqlite的连接字符串与MySql和SqlServer明显不同,需要直接连接到Sqlite的数据库的db文件,如果忽略后缀.db则无法连接成功。
传入一个数据库db文件(Sqlite数据库需要指定数据库文件的位置)以及一个数据库连接配置(MySql数据库及SqlServer数据库连接字符串需要id,password,port等配置)我们可以创建一个数据库连接,这里以MySql数据库为例,代码如下:
c#
/// <summary>
///创建数据库连接
///</summary>
/// <param name="databasePath">The database path.</param>
/// <param name="connectionConfig">The connection config.</param>
public override void CreateDbConnection(string databasePath, DatabaseConnectionConfig connectionConfig)
{
try
{
var connectionString = $"Server={connectionConfig.ip};Port={connectionConfig.port};Database={databaseName};User Id={connectionConfig.id};Password={connectionConfig.password};";
mySqlConnection = new MySqlConnection(connectionString);
mySqlConnection.Open();
CreateTable();
Debug.Log("数据库连接成功");
}
catch (MySqlException e)
{
Debug.LogError($"数据库连接失败:{e.Message}");
}
}
2.3数据库建表
连接上数据库之后,我们就需要建表(也可以在数据库端手动进行建表然后连接),这里说下我引用的dll文件中,MySql和SqlServer数据库的sqlConnection以及sqlCommand等都是继承自System.Data.Common命名空间的相关类,而Sqlite则不同,他是引用SQLite.cs文件的SQLite4Unity3d命名空间,相关的类并不类似MySql和SqlServer继承自System.Data.Common命名空间的相关类。一方面SQLite4Unity3d中对数据的连接、创建表以及增删改查等方法进行了封装,使用非常方便,另外一个重要原因就是我使用System.Data.SQLite进行数据库连接,在unity中进行打包时会报错。
下面,我主要以MySql为例,贴一下建表代码如下,与SqlServer差不多,其中id、dateTime、result等就是数据表中的列名,INT是数据类型,PRIMARY KEY是将id声明为主键,AUTO_INCREMENT定义主键自增。
c#
/// <summary>
/// The create table.
/// </summary>
public override void CreateTable()
{
var createTableSql = $@"
CREATE TABLE IF NOT EXISTS {tableName} (
{id} INT PRIMARY KEY AUTO_INCREMENT,
{specimenType} VARCHAR(50),
{qrCode} VARCHAR(100),
{dateTime} DATETIME,
{manipulator} VARCHAR(50),
{result} VARCHAR(50),
{dataPath} VARCHAR(50)
)";
MySqlCommand command = new MySqlCommand(createTableSql, mySqlConnection);
command.ExecuteNonQuery();
}
Sqlite的建表代码如下,非常简洁,DatabaseTableStruct为数据结构类:
c#
/// <summary>
/// The create table.
/// </summary>
public override void CreateTable()
{
sqLiteConnection.CreateTable<DatabaseTableStruct>();
}
2.4插入数据
插入数据传入一个要插入的数据,这里需要注意,主键我们已经定义为自增,不需要对主键id进行赋值,插入诗句时会主动为id分配一个唯一值。插入数据时对id值进行赋值若违反唯一性原则,会插入数据失败。主要代码如下:
c#
/// <summary>
/// 插入数据
/// </summary>
/// <param name="detectionData">The detection data.</param>
public override void ExecuteInsert(DatabaseTableStruct detectionData)
{
// 使用参数化查询来防止SQL注入
var query = $"INSERT INTO {tableName} ({specimenType}, {qrCode}, {dateTime}, {manipulator}, {result}, {dataPath}) " +
$"VALUES (@{specimenType}, @{qrCode}, @{dateTime}, @{manipulator}, @{result}, @{dataPath}";
MySqlCommand command = new MySqlCommand(query, this.mySqlConnection);
// 为参数设置值
command.Parameters.AddWithValue($"@{specimenType}", detectionData.specimenType);
command.Parameters.AddWithValue($"@{qrCode}", detectionData.qrCode);
command.Parameters.AddWithValue($"@{dateTime}", detectionData.dateTime);
command.Parameters.AddWithValue($"@{manipulator}", detectionData.manipulator);
command.Parameters.AddWithValue($"@{result}", detectionData.result);
command.Parameters.AddWithValue($"@{dataPath}", detectionData.dataPath);
command.ExecuteNonQuery();
}
2.5删除数据
删除数据传入一个要删除的数据,根据主键id值(唯一性)来删除数据,主要代码如下:
c#
/// <summary>
/// The execute delete.
/// </summary>
/// <param name="detectionData">The detection data.</param>
public override void ExecuteDelete(DatabaseTableStruct detectionData)
{
var query = $"DELETE FROM {tableName} WHERE {id} = @{id}";
MySqlCommand command = new MySqlCommand(query, this.mySqlConnection);
// 为参数设置值
command.Parameters.AddWithValue($"@{id}", detectionData.id);
command.ExecuteNonQuery();
}
2.6更新(修改)数据
更新数据传入一个要更新的数据,根据主键id值来修改并更新数据值,主要代码如下:
c#
/// <summary>
/// The execute modify.
/// </summary>
/// <param name="detectionData">The detection data.</param>
public override void ExecuteUpdate(DatabaseTableStruct detectionData)
{
// 根据主键 id 更新数据
var query = $"UPDATE {tableName} " +
$"SET {specimenType} = @{specimenType}, {qrCode} = @{qrCode}, {dateTime} = @{dateTime}, {manipulator} = @{manipulator}, {result} = @{result}, {dataPath} = @{dataPath} " +
$"WHERE {id} = @{id}";
MySqlCommand command = new MySqlCommand(query, this.mySqlConnection);
// 为参数设置值
command.Parameters.AddWithValue($"@{specimenType}", detectionData.specimenType);
command.Parameters.AddWithValue($"@{qrCode}", detectionData.qrCode);
command.Parameters.AddWithValue($"@{dateTime}", detectionData.dateTime);
command.Parameters.AddWithValue($"@{manipulator}", detectionData.manipulator);
command.Parameters.AddWithValue($"@{result}", detectionData.result);
command.Parameters.AddWithValue($"@{dataPath}", detectionData.dataPath);
command.Parameters.AddWithValue($"@{id}", detectionData.id);
command.ExecuteNonQuery();
}
2.7查询数据
查询数据传入你需要的查询条件,可以一个或者多个条件,根据需求而定,主要代码(参考)如下:
c#
/// <summary>
/// The execute query.
/// </summary>
/// <param name="dateTimeFrom">The data time from.</param>
/// <param name="dateTimeTo">The data time to.</param>
/// <param name="filterType">The filter type.</param>
/// <param name="param">The param.</param>
/// <param name="detectionResult">The detectionResult.</param>
public override void ExecuteQuery(
DateTime dateTimeFrom,
DateTime dateTimeTo,
DataBaseFilterType filterType,
string param,
DetectResult detectionResult)
{
List<DatabaseTableStruct> resultList = new List<DatabaseTableStruct>();
string query;
if (dateTimeFrom == DateTime.MaxValue && dateTimeTo == DateTime.MaxValue)
{
//这里可以看成日期没有选择的情况下查询该数据库该表中对应的所有数据
query = $"SELECT * FROM {tableName}";
}
else
{
//若选择了日期,则根据日期范围来查询数据
query = $"SELECT * FROM {tableName}" + $" WHERE {dateTime}>=@dateTimeFrom AND {dateTime}<@dateTimeTo";
}
MySqlCommand command = new MySqlCommand(query, this.mySqlConnection);
// 为参数设置值
command.Parameters.AddWithValue("@dateTimeFrom", dateTimeFrom);
command.Parameters.AddWithValue("@dateTimeTo", dateTimeTo);
MySqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
// 从查询结果中读取数据并将其转化为适当的对象
DatabaseTableStruct data = new DatabaseTableStruct
{
id = reader.GetInt32(reader.GetOrdinal(id)),
specimenType = reader.GetString(reader.GetOrdinal(specimenType)),
qrCode = reader.GetString(reader.GetOrdinal(qrCode)),
dateTime = reader.GetDateTime(reader.GetOrdinal(dateTime)),
manipulator = reader.GetString(reader.GetOrdinal(manipulator)),
result = reader.GetString(reader.GetOrdinal(result)),
dataPath = reader.GetString(reader.GetOrdinal(dataPath))
};
resultList.Add(data);
}
//数据查询完成关闭reader
reader.Close();
//为了方便,在根据日期筛选出数据集合后,根据LINQ语句来查询其余条件的数据
if (!string.IsNullOrEmpty(param))
{
switch (filterType)
{
case DataBaseFilterType.SpecimenType:
resultList = resultList.Where(p => p.specimenType == param).ToList();
break;
case DataBaseFilterType.QrCode:
resultList = resultList.Where(p => p.qrCode == param).ToList();
break;
case DataBaseFilterType.Manipulator:
resultList = resultList.Where(p => p.manipulator == param).ToList();
break;
}
}
switch (detectionResult)
{
case DetectResult.Ng:
resultList = resultList.Where(p => p.result == "NG").ToList();
break;
case DetectResult.Ok:
resultList = resultList.Where(p => p.result == "OK").ToList();
break;
}
this.dataLists = resultList;
}
2.8关闭数据库连接
最后,关闭应用程序或者切换数据库时,我们需要关闭现有的连接,再去创建新的连接。
c#
/// <summary>
/// The dispose.
/// </summary>
public override void Dispose()
{
if (mySqlConnection != null)
{
mySqlConnection.Close();
}
}
3、结语
由于篇幅有限,详细代码及dll文件等下载地址链接:pan.baidu.com/s/1asWNBYoP... ,提取码:iq99
上述代码均在项目中成功实践,打包在在windows运行正常能连接各数据库且进行增删改查。水平有限,若有错误望各位大佬批评指正!若有更好的方法方式,欢迎大家评论区交流!