在 C# 中处理 SQLite - 将传递字符串操作作为命令推进
Handling SQLite in C# - advancing pass string manipulations as commands
我在我的应用程序中使用 SQLite(通过 System.Data.SQLite 包)。现在所有的插入、查询和其他操作都是通过使用字符串发送命令来完成的,例如:
SQLiteCommand command = new SQLiteCommand(comStr, db);
其中 comStr - 是保存命令的字符串变量。
我可以使用其他选项来代替字符串吗?或者字符串是处理来自 .NET 的 SQL 查询时应该使用的正确方法吗?
问题是使用字符串会变得相当混乱,例如我有一些用户可以设置的过滤器。使用字符串操作命令 - 虽然有效 - 对我来说感觉非常脆弱:
public string GetFilterString()
{
string fil1 = "";
string fil2 = "";
string fil3 = "";
string fil4 = "";
// filter by time
switch (WithinTimeBtnStatus)
{
case WithinTime.All:
break;
case WithinTime.Hour:
string minusHour = (DateTime.Now - new TimeSpan(0, 1, 0, 0)).ToString("yyyy-MM-dd HH:mm:ss.fff");
fil1 = $" timestamp >= datetime('{minusHour}')";
break;
case WithinTime.Day:
string minusDay = (DateTime.Now - new TimeSpan(1, 0, 0, 0)).ToString("yyyy-MM-dd HH:mm:ss.fff");
fil1 = $" timestamp >= datetime('{minusDay}')";
break;
case WithinTime.Week:
string minusWeek = (DateTime.Now - new TimeSpan(7, 0, 0, 0)).ToString("yyyy-MM-dd HH:mm:ss.fff");
fil1 = $" timestamp >= datetime('{minusWeek}')";
break;
}
// filter by extension
for (int i = 0; i < FilteredExt.Count; i++)
{
fil2 += " ext != '" + FilteredExt[i] + "'";
if (i < FilteredExt.Count - 1)
fil2 += " and";
}
// filter by process
if (_processFilterSelected.ToLower() != "all" && _processFilterSelected != "")
{
fil3 = $" proc == '{_processFilterSelected}'";
}
// filter by File Operation
if (_FileOperationFilterSelected.ToLower() != "all" && _FileOperationFilterSelected != "")
{
FileOperation fo = Converters.StringToFileOperation(_FileOperationFilterSelected);
switch (fo)
{
case FileOperation.Deleted:
fil4 = " oper == 'DELETED'";
break;
case FileOperation.Renamed:
fil4 = " oper == 'RENAMED'";
break;
case FileOperation.Modified:
fil4 = " oper == 'MODIFIED'";
break;
}
}
string fil = "";
var tmp = new[] { fil1, fil2, fil3, fil4 };
foreach (var t in tmp)
{
if (t != "")
{
fil += " and" + t;
}
}
return fil;
}
编辑以提供一些解决方案作为答案。
本教程向您展示如何正确实施 SQLite 并使用 Linq 扩展与您的数据库 table 交互。我已经复制了下面的相关部分。打开数据库连接并首先创建数据 tables 后,您就可以与 table 交互,就像使用 Linq 与任何 IEnumerable 交互一样。它还提供了将 SQL 作为字符串传递的选项,但是由于这在编译时未检查,您 运行 有 运行 时间错误的风险。
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/databases/
TodoItemDatabase 构造函数如下所示:
public TodoItemDatabase(string dbPath)
{
database = new SQLiteAsyncConnection(dbPath);
database.CreateTableAsync<TodoItem>().Wait();
}
此方法创建一个单一的数据库连接,该连接在应用程序 运行 时保持打开状态,因此避免了每次执行数据库操作时打开和关闭数据库文件的开销。
TodoItemDatabase 的其余部分 class 包含 SQLite 查询 运行 跨平台。示例查询代码如下所示(有关语法的更多详细信息,请参阅使用 SQLite.NET 一文):
public Task<List<TodoItem>> GetItemsAsync()
{
return database.Table<TodoItem>().ToListAsync();
}
public Task<List<TodoItem>> GetItemsNotDoneAsync()
{
return database.QueryAsync<TodoItem>("SELECT * FROM [TodoItem] WHERE [Done] = 0");
}
public Task<TodoItem> GetItemAsync(int id)
{
return database.Table<TodoItem>().Where(i => i.ID == id).FirstOrDefaultAsync();
}
public Task<int> SaveItemAsync(TodoItem item)
{
if (item.ID != 0)
{
return database.UpdateAsync(item);
}
else {
return database.InsertAsync(item);
}
}
public Task<int> DeleteItemAsync(TodoItem item)
{
return database.DeleteAsync(item);
}
既然我没有得到满意的答复,我会post我最后做的。我认为这可能是一种不错的方法,但可能还有其他更好的方法来实现我正在寻找的东西(在我的数据库上使用 LINQ 类型语法,而不是使用包含查询的字符串)。
此外 - 我不确定这是否比使用查询字符串更快。
TL;DR:使用 SQLite.CodeFirst + EntityFramework。
(附带说明,可能可以使用 LINQ 来 SQL 而不是 EntityFramework,但不确定是否也在 CodeFirst 方法中。一旦我尝试并测试了这个就会更新).
首先,您需要添加这些包:
- Entity Framework
- System.Data.SQLite(可能还会安装:)
- System.Data.SQLite.核心
- System.Data.SQLite.EF6
- System.Data.SQLite.Linq
最后,如果您是从代码开始的(就像我一样),您还需要
- SQLite.CodeFirst
接下来要做的是在 app.config 中设置连接字符串。
(附带说明 - 指定提供程序似乎存在大量错误,卸载并重新安装上面的包似乎可以修复。我不太确定删除和添加以及不变性背后的逻辑名字 - 如果你喜欢我写的东西,那就是:
<system.data>
<DbProviderFactories>
<remove invariant="System.Data.SQLite.EF6" />
<add name="SQLite Data Provider (Entity Framework 6)" invariant="System.Data.SQLite.EF6" description=".NET Framework Data Provider for SQLite (Entity Framework 6)" type="System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6" />
<remove invariant="System.Data.SQLite" />
<add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".NET Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
</DbProviderFactories>
</system.data>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
<providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
<provider invariantName="System.Data.SQLite.EF6" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
<provider invariantName="System.Data.SQLite" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6"/>
</providers>
</entityFramework>
)
连接字符串应指定名称,并且至少应指定数据库文件所在位置的路径。您还可以使用稍后在代码中定义的相对路径(通过使用 |DataDirectory| 语法):
<connectionStrings>
<add name="YourModel" connectionString="Data Source=|DataDirectory|\NameOfYourDBFile.sqlite" providerName="System.Data.SQLite" />
</connectionStrings>
下一步,如果您正在执行代码优先,则创建一个新的 class 作为您的模型,这基本上是使用 SQLite.CodeFirst 包:
class YourModel : DbContext
{
// Your context has been configured to use a 'YourModel' connection string from your application's
// configuration file (App.config or Web.config). By default, this connection string targets the
// 'YourProject.YourModel' database on your LocalDb instance.
//
// If you wish to target a different database and/or database provider, modify the 'YourModel'
// connection string in the application configuration file.
public YourModel()
: base("name=YourModel")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var sqliteConnectionInitializer = new SqliteCreateDatabaseIfNotExists<YourModel>(modelBuilder);
Database.SetInitializer(sqliteConnectionInitializer);
Database.SetInitializer(new SqliteDropCreateDatabaseWhenModelChanges<YourModel>(modelBuilder));
}
// Add a DbSet for each entity type that you want to include in your model. For more information
// on configuring and using a Code First model, see http://go.microsoft.com/fwlink/?LinkId=390109.
public virtual DbSet<YourTableClass> YourTable { get; set; }
}
[Table("YourTable")]
public class YourTableClass
{
[Key]
public string Id { get; set; }
[Required]
public FileOperation Oper { get; set; }
[Required, Index]
public OperationState State { get; set; }
[Index]
public string Proc { get; set; }
[Required]
public string Src { get; set; }
public DateTime Timestamp { get; set; }
// etc.
}
}
您可以阅读更多相关信息 here。
准备工作基本上就是这样。
(附带说明,如果你想从代码后面更改数据库文件的相对路径,你需要写:
AppDomain.CurrentDomain.SetData("DataDirectory", @"the\path\you\desire");
)
现在您可以直接使用它了。基本语法非常简单,你只需使用:
using (var context = new YourModel())
{
// some query
}
所以Select:
using (var context = new YourModel())
{
var t = context.YourTable
.Where(e => e.State == OperationState.BackedUp)
.Select(e => e.Proc)
.Distinct()
}
如果你想插入:
using (var context = new YourModel())
{
var e = context.YourTable.Create();
e.Id = guid;
// ...etc
e.Timestamp = timestamp;
context.YourTable.Add(e);
context.SaveChanges();
}
如果您仍想使用字符串查询:
using (var context = new YourModel())
{
context.Database.ExecuteSqlCommand(comString);
}
需要记住的一些重要事项:
- 如果您在数据库中更改某些内容,则最后必须调用
Context.SaveChange()
(ExecuteSqlCommand 不需要这个)
- 可以使用 RemoveRange + SaveChange() 或仍然使用查询字符串来完成删除。
因此问题 GetFilterString 中的示例更改为:
public static IQueryable<YourTable> GetFilteredQueryable(IQueryable<YourTable> yourTable)
{
// filter by time
switch (RestoreLogic.WithinTimeBtnStatus)
{
case WithinTime.All:
break;
case WithinTime.Hour:
DateTime offsetHour = DateTime.Now.Add(new TimeSpan(-1, 0, 0));
yourTable = yourTable.Where(e => e.Timestamp >= offsetHour);
break;
// etc.
}
// filter by extension
foreach (var i in FilteredExt)
{
yourTable = yourTable.Where(e => e.Ext != i);
}
// etc.
return yourTable;
}
我在我的应用程序中使用 SQLite(通过 System.Data.SQLite 包)。现在所有的插入、查询和其他操作都是通过使用字符串发送命令来完成的,例如:
SQLiteCommand command = new SQLiteCommand(comStr, db);
其中 comStr - 是保存命令的字符串变量。
我可以使用其他选项来代替字符串吗?或者字符串是处理来自 .NET 的 SQL 查询时应该使用的正确方法吗?
问题是使用字符串会变得相当混乱,例如我有一些用户可以设置的过滤器。使用字符串操作命令 - 虽然有效 - 对我来说感觉非常脆弱:
public string GetFilterString()
{
string fil1 = "";
string fil2 = "";
string fil3 = "";
string fil4 = "";
// filter by time
switch (WithinTimeBtnStatus)
{
case WithinTime.All:
break;
case WithinTime.Hour:
string minusHour = (DateTime.Now - new TimeSpan(0, 1, 0, 0)).ToString("yyyy-MM-dd HH:mm:ss.fff");
fil1 = $" timestamp >= datetime('{minusHour}')";
break;
case WithinTime.Day:
string minusDay = (DateTime.Now - new TimeSpan(1, 0, 0, 0)).ToString("yyyy-MM-dd HH:mm:ss.fff");
fil1 = $" timestamp >= datetime('{minusDay}')";
break;
case WithinTime.Week:
string minusWeek = (DateTime.Now - new TimeSpan(7, 0, 0, 0)).ToString("yyyy-MM-dd HH:mm:ss.fff");
fil1 = $" timestamp >= datetime('{minusWeek}')";
break;
}
// filter by extension
for (int i = 0; i < FilteredExt.Count; i++)
{
fil2 += " ext != '" + FilteredExt[i] + "'";
if (i < FilteredExt.Count - 1)
fil2 += " and";
}
// filter by process
if (_processFilterSelected.ToLower() != "all" && _processFilterSelected != "")
{
fil3 = $" proc == '{_processFilterSelected}'";
}
// filter by File Operation
if (_FileOperationFilterSelected.ToLower() != "all" && _FileOperationFilterSelected != "")
{
FileOperation fo = Converters.StringToFileOperation(_FileOperationFilterSelected);
switch (fo)
{
case FileOperation.Deleted:
fil4 = " oper == 'DELETED'";
break;
case FileOperation.Renamed:
fil4 = " oper == 'RENAMED'";
break;
case FileOperation.Modified:
fil4 = " oper == 'MODIFIED'";
break;
}
}
string fil = "";
var tmp = new[] { fil1, fil2, fil3, fil4 };
foreach (var t in tmp)
{
if (t != "")
{
fil += " and" + t;
}
}
return fil;
}
编辑以提供一些解决方案作为答案。
本教程向您展示如何正确实施 SQLite 并使用 Linq 扩展与您的数据库 table 交互。我已经复制了下面的相关部分。打开数据库连接并首先创建数据 tables 后,您就可以与 table 交互,就像使用 Linq 与任何 IEnumerable 交互一样。它还提供了将 SQL 作为字符串传递的选项,但是由于这在编译时未检查,您 运行 有 运行 时间错误的风险。
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/databases/
TodoItemDatabase 构造函数如下所示:
public TodoItemDatabase(string dbPath)
{
database = new SQLiteAsyncConnection(dbPath);
database.CreateTableAsync<TodoItem>().Wait();
}
此方法创建一个单一的数据库连接,该连接在应用程序 运行 时保持打开状态,因此避免了每次执行数据库操作时打开和关闭数据库文件的开销。 TodoItemDatabase 的其余部分 class 包含 SQLite 查询 运行 跨平台。示例查询代码如下所示(有关语法的更多详细信息,请参阅使用 SQLite.NET 一文):
public Task<List<TodoItem>> GetItemsAsync()
{
return database.Table<TodoItem>().ToListAsync();
}
public Task<List<TodoItem>> GetItemsNotDoneAsync()
{
return database.QueryAsync<TodoItem>("SELECT * FROM [TodoItem] WHERE [Done] = 0");
}
public Task<TodoItem> GetItemAsync(int id)
{
return database.Table<TodoItem>().Where(i => i.ID == id).FirstOrDefaultAsync();
}
public Task<int> SaveItemAsync(TodoItem item)
{
if (item.ID != 0)
{
return database.UpdateAsync(item);
}
else {
return database.InsertAsync(item);
}
}
public Task<int> DeleteItemAsync(TodoItem item)
{
return database.DeleteAsync(item);
}
既然我没有得到满意的答复,我会post我最后做的。我认为这可能是一种不错的方法,但可能还有其他更好的方法来实现我正在寻找的东西(在我的数据库上使用 LINQ 类型语法,而不是使用包含查询的字符串)。
此外 - 我不确定这是否比使用查询字符串更快。
TL;DR:使用 SQLite.CodeFirst + EntityFramework。
(附带说明,可能可以使用 LINQ 来 SQL 而不是 EntityFramework,但不确定是否也在 CodeFirst 方法中。一旦我尝试并测试了这个就会更新).
首先,您需要添加这些包:
- Entity Framework
- System.Data.SQLite(可能还会安装:)
- System.Data.SQLite.核心
- System.Data.SQLite.EF6
- System.Data.SQLite.Linq
最后,如果您是从代码开始的(就像我一样),您还需要
- SQLite.CodeFirst
接下来要做的是在 app.config 中设置连接字符串。
(附带说明 - 指定提供程序似乎存在大量错误,卸载并重新安装上面的包似乎可以修复。我不太确定删除和添加以及不变性背后的逻辑名字 - 如果你喜欢我写的东西,那就是:
<system.data>
<DbProviderFactories>
<remove invariant="System.Data.SQLite.EF6" />
<add name="SQLite Data Provider (Entity Framework 6)" invariant="System.Data.SQLite.EF6" description=".NET Framework Data Provider for SQLite (Entity Framework 6)" type="System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6" />
<remove invariant="System.Data.SQLite" />
<add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".NET Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
</DbProviderFactories>
</system.data>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
<providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
<provider invariantName="System.Data.SQLite.EF6" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
<provider invariantName="System.Data.SQLite" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6"/>
</providers>
</entityFramework>
)
连接字符串应指定名称,并且至少应指定数据库文件所在位置的路径。您还可以使用稍后在代码中定义的相对路径(通过使用 |DataDirectory| 语法):
<connectionStrings>
<add name="YourModel" connectionString="Data Source=|DataDirectory|\NameOfYourDBFile.sqlite" providerName="System.Data.SQLite" />
</connectionStrings>
下一步,如果您正在执行代码优先,则创建一个新的 class 作为您的模型,这基本上是使用 SQLite.CodeFirst 包:
class YourModel : DbContext
{
// Your context has been configured to use a 'YourModel' connection string from your application's
// configuration file (App.config or Web.config). By default, this connection string targets the
// 'YourProject.YourModel' database on your LocalDb instance.
//
// If you wish to target a different database and/or database provider, modify the 'YourModel'
// connection string in the application configuration file.
public YourModel()
: base("name=YourModel")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var sqliteConnectionInitializer = new SqliteCreateDatabaseIfNotExists<YourModel>(modelBuilder);
Database.SetInitializer(sqliteConnectionInitializer);
Database.SetInitializer(new SqliteDropCreateDatabaseWhenModelChanges<YourModel>(modelBuilder));
}
// Add a DbSet for each entity type that you want to include in your model. For more information
// on configuring and using a Code First model, see http://go.microsoft.com/fwlink/?LinkId=390109.
public virtual DbSet<YourTableClass> YourTable { get; set; }
}
[Table("YourTable")]
public class YourTableClass
{
[Key]
public string Id { get; set; }
[Required]
public FileOperation Oper { get; set; }
[Required, Index]
public OperationState State { get; set; }
[Index]
public string Proc { get; set; }
[Required]
public string Src { get; set; }
public DateTime Timestamp { get; set; }
// etc.
}
}
您可以阅读更多相关信息 here。
准备工作基本上就是这样。
(附带说明,如果你想从代码后面更改数据库文件的相对路径,你需要写:
AppDomain.CurrentDomain.SetData("DataDirectory", @"the\path\you\desire");
)
现在您可以直接使用它了。基本语法非常简单,你只需使用:
using (var context = new YourModel())
{
// some query
}
所以Select:
using (var context = new YourModel())
{
var t = context.YourTable
.Where(e => e.State == OperationState.BackedUp)
.Select(e => e.Proc)
.Distinct()
}
如果你想插入:
using (var context = new YourModel())
{
var e = context.YourTable.Create();
e.Id = guid;
// ...etc
e.Timestamp = timestamp;
context.YourTable.Add(e);
context.SaveChanges();
}
如果您仍想使用字符串查询:
using (var context = new YourModel())
{
context.Database.ExecuteSqlCommand(comString);
}
需要记住的一些重要事项:
- 如果您在数据库中更改某些内容,则最后必须调用
Context.SaveChange()
(ExecuteSqlCommand 不需要这个) - 可以使用 RemoveRange + SaveChange() 或仍然使用查询字符串来完成删除。
因此问题 GetFilterString 中的示例更改为:
public static IQueryable<YourTable> GetFilteredQueryable(IQueryable<YourTable> yourTable)
{
// filter by time
switch (RestoreLogic.WithinTimeBtnStatus)
{
case WithinTime.All:
break;
case WithinTime.Hour:
DateTime offsetHour = DateTime.Now.Add(new TimeSpan(-1, 0, 0));
yourTable = yourTable.Where(e => e.Timestamp >= offsetHour);
break;
// etc.
}
// filter by extension
foreach (var i in FilteredExt)
{
yourTable = yourTable.Where(e => e.Ext != i);
}
// etc.
return yourTable;
}