System.Data.SQLite的Close()方法不能释放数据库文件

125

在尝试删除文件之前,我关闭数据库时遇到了问题。 代码只是这样的

 myconnection.Close();    
 File.Delete(filename);

执行Delete()方法时抛出了“文件仍在使用”的异常。我在调试器中等待几分钟后重新尝试了Delete()方法,因此这不是时间问题。

我有事务代码,但在Close()方法调用之前没有运行它。所以我相当确定它不是一个未关闭的事务。在Open()和Close()之间的SQL命令只是查询语句。

ProcMon显示我的程序和杀毒软件正在查看数据库文件。它没有显示我的程序在Close()方法之后释放数据库文件。

使用的工具为Visual Studio 2010,编程语言是C#,System.Data.SQLite版本为1.0.77.0,操作系统为Win7。

我看到了一个两年前的类似问题,但更新日志中说已经修复了。

还有其他我可以检查的吗?是否有一种方法可以获取所有打开的命令或事务的列表?


新的、可行的代码:

 db.Close();
 GC.Collect();   // yes, really release the db

 bool worked = false;
 int tries = 1;
 while ((tries < 4) && (!worked))
 {
    try
    {
       Thread.Sleep(tries * 100);
       File.Delete(filename);
       worked = true;
    }
    catch (IOException e)   // delete only throws this on locking
    {
       tries++;
    }
 }
 if (!worked)
    throw new IOException("Unable to close file" + filename);

1
你尝试过以下代码吗: myconnection.Close(); myconnection.Dispose(); - Przemysław Michalski
1
使用 sqlite-net 时,您可以使用 SQLiteAsyncConnection.ResetPool(),有关详细信息,请参见此问题 - Uwe Keim
18个回答

140

我之前在为C#编写一个数据库抽象层时遇到了同样的问题,但我从未找到原因。最终我只是在使用我的库尝试删除SQLite数据库时抛出了一个异常。

无论如何,今天下午我再次回顾了所有内容,并决定彻底找出它为什么会这样做,以下是我目前发现的情况。

当您调用SQLiteConnection.Close()时,会释放指向SQLite数据库实例的SQLiteConnectionHandle(连同一些检查和其他操作)。这是通过调用SQLiteConnectionHandle.Dispose()完成的,但直到CLR的垃圾回收器执行某些垃圾回收时才会实际释放指针。由于SQLiteConnectionHandle重载了CriticalHandle.ReleaseHandle()函数以调用sqlite3_close_interop(),因此这不会关闭数据库。

我认为这种做法非常糟糕,因为程序员无法确切知道数据库何时关闭,但这是它被做成的方式,所以我想我们现在只能忍受它,或者对System.Data.SQLite进行一些更改。欢迎任何志愿者来做这件事,但很遗憾,我在明年之前没有时间这样做。

TL;DR 解决方法是在调用SQLiteConnection.Close()后和调用File.Delete()之前强制进行垃圾回收。

下面是示例代码:

string filename = "testFile.db";
SQLiteConnection connection = new SQLiteConnection("Data Source=" + filename + ";Version=3;");
connection.Close();
GC.Collect();
GC.WaitForPendingFinalizers();
File.Delete(filename);

祝你好运,并希望它能帮到你。


1
是的!谢谢!看起来垃圾回收可能需要一点时间来完成它的工作。 - Tom Cerul
1
我知道这很古老了,但感谢你帮我避免了一些痛苦。这个 bug 也会影响 SQLite 在 Windows Mobile / Compact Framework 下的构建。 - StrayPointer
2
干得好!立即解决了我的问题。在11年的C#开发中,我从未有过使用GC.Collect的需要:现在这是我被迫这样做的第一个例子。 - Pilsator
25
GC.Collect(); 可以起作用,但是使用 System.Data.SQLite.SQLiteConnection.ClearAllPools(); 使用该库的API可以解决这个问题。 - Aaron Hudon
2
还需要注意,如果您有打开的读取器,请将其关闭。如果您在连接上调用Dispose并且所有命令调用时也可以跳过“GC.Collect()”选项。 - exts
显示剩余5条评论

55

仅使用GC.Collect()对我没有起到作用。

我不得不在GC.Collect()之后添加GC.WaitForPendingFinalizers()才能继续进行文件删除。


7
这并不令人惊讶,GC.Collect() 只是启动异步垃圾回收,所以为了确保所有内容都已清理干净,您需要明确等待它完成。 - ChrisWue
2
我也遇到了同样的问题,不得不添加GC.WaitForPendingFinalizers()。这是在1.0.103版本中发生的。 - Vort3x

29

以下方法对我有效:

MySQLiteConnection.Close();
SQLite.SQLiteConnection.ClearAllPools()

更多信息: SQLite通过连接池来提高性能。这意味着当您在连接对象上调用Close方法时,与数据库的连接可能仍然存在(在后台),以便下一个Open方法变得更快。当您确定不再需要新连接时,调用ClearAllPools将关闭后台仍然存在的所有连接并释放对db文件的文件句柄。然后db文件可能会被删除或被其他进程使用。


1
请您能否解释一下为什么这是解决问题的好方法。 - Matas Vaitkevicius
你也可以使用 SQLiteConnectionPool.Shared.Reset()。这将关闭所有打开的连接。特别是,如果你使用的是没有 Close() 方法的 SQLiteAsyncConnection,那么这是一个解决方案。 - Lorenzo Polidori
不幸的是,SQLiteConnectionPoolinternal 的,因此不能直接使用(除非使用反射)。 - Sebastian Krysmanski
ClearAllPools 对我很有用,当等待终结器无法解决问题时。谢谢。 - billpg

23

我遇到了类似的问题,虽然垃圾收集器的解决方案没有修复它。

发现在使用后处理SQLiteCommandSQLiteDataReader对象可避免使用垃圾收集器。

SQLiteCommand command = new SQLiteCommand(sql, db);
command.ExecuteNonQuery();
command.Dispose();

2
没错。确保即使您稍后重用 SQLiteCommand 变量,也要处理每个 SQLiteCommand - Bruno Bieri
这对我起作用了。我还确保处理了任何交易。 - Jay-Nicolas Hackleman
2
太好了!你为我节省了不少时间。将command.Dispose();添加到执行的每个SQLiteCommand中,修复了错误。 - Ivan B
另外,请确保您释放(即.Dispose())其他对象,如SQLiteTransaction(如果有的话)。 - Ivan B

21
在我的情况下,我创建了SQLiteCommand对象却没有明确地将它们处理掉。
var command = connection.CreateCommand();
command.CommandText = commandText;
value = command.ExecuteScalar();

我在一个 using 语句中包装了我的命令,这解决了我的问题。

static public class SqliteExtensions
{
    public static object ExecuteScalar(this SQLiteConnection connection, string commandText)
    {
        using (var command = connection.CreateCommand())
        {
            command.CommandText = commandText;
            return command.ExecuteScalar();
        }
    }
}

using语句确保即使发生异常也会调用Dispose方法。

这样执行命令也容易得多。

value = connection.ExecuteScalar(commandText)
// Command object created and disposed

7
我强烈建议不要忽视这样的异常情况而将其置之不理。 - Tom McKearney
它对我起作用了,尽管我也尝试了上面提到的所有其他建议,但那些建议对我没有起作用。即使我非常反对吞咽异常,在我的情况下它会创建问题。我真的希望文件能够被释放并稍后删除。 - Sameer Rathoud
@SameerRathoud,之前我在代码中加了一个try-catch(可以从历史记录中看到),但现在已经移除了。不应再有任何异常被忽略。现在的代码将抛出这些异常。 - Nate

9

我曾经遇到过类似的问题,尝试使用 GC.Collect 解决,但是如上所述,这可能需要很长时间才能使文件不再被锁定。

我找到了一种替代方案,涉及到在 TableAdapters 中处理底层的 SQLiteCommand,请参见 此答案 以获取更多信息。


你是对的!在某些情况下,简单的'GC.Collect'适用于我,但在其他情况下,在调用GC.Collect之前必须处理与连接相关联的任何SqliteCommands,否则它将不起作用! - Eitan H.S.
2
在SQLiteCommand上调用Dispose对我很有帮助。另外一条评论是,如果你正在调用GC.Collect,那么你做错了什么。 - Natalie Adams
@NathanAdams 当使用EntityFramework时,您永远不会处理任何单个命令对象。因此,无论是EntityFramework本身还是用于EF包装的SQLite都存在问题。 - springy76
你的答案应该是正确的。非常感谢。 - Ahmed Shamel

7

我在使用Entity Framework和System.Data.Sqlite时遇到了相同的问题。

我发现使用SQLiteConnection.ClearAllPools()和GC.Collect()可以减少文件锁定的频率,但仍会偶尔出现(大约1%的时间)。

我进行了调查,发现一些EF创建的SQLiteCommands没有被释放,并且它们的Connection属性仍设置为已关闭的连接。我尝试对它们进行处理,但下一次的DbContext读取时,Entity Framework会抛出异常——似乎EF有时仍在连接关闭后使用它们。

我的解决方案是确保这些SQLiteCommands在连接关闭时将Connection属性设置为Null。这似乎足以释放文件锁定。我测试了以下代码,在进行了数千次测试后没有看到任何文件锁定问题:

public static class ClearSQLiteCommandConnectionHelper
{
    private static readonly List<SQLiteCommand> OpenCommands = new List<SQLiteCommand>();

    public static void Initialise()
    {
        SQLiteConnection.Changed += SqLiteConnectionOnChanged;
    }

    private static void SqLiteConnectionOnChanged(object sender, ConnectionEventArgs connectionEventArgs)
    {
        if (connectionEventArgs.EventType == SQLiteConnectionEventType.NewCommand && connectionEventArgs.Command is SQLiteCommand)
        {
            OpenCommands.Add((SQLiteCommand)connectionEventArgs.Command);
        }
        else if (connectionEventArgs.EventType == SQLiteConnectionEventType.DisposingCommand && connectionEventArgs.Command is SQLiteCommand)
        {
            OpenCommands.Remove((SQLiteCommand)connectionEventArgs.Command);
        }

        if (connectionEventArgs.EventType == SQLiteConnectionEventType.Closed)
        {
            var commands = OpenCommands.ToList();
            foreach (var cmd in commands)
            {
                if (cmd.Connection == null)
                {
                    OpenCommands.Remove(cmd);
                }
                else if (cmd.Connection.State == ConnectionState.Closed)
                {
                    cmd.Connection = null;
                    OpenCommands.Remove(cmd);
                }
            }
        }
    }
}

只需在应用程序加载开始时调用ClearSQLiteCommandConnectionHelper.Initialise();,即可使用该功能。这将保留活动命令的列表,并在它们指向已关闭的连接时将其连接设置为Null


我还必须在 DisposingCommand 部分将连接设置为 null,否则偶尔会出现 ObjectDisposedExceptions。 - Vae
1
在我看来,这是一个被低估的答案。它解决了我因为EF层而无法自己完成的清理问题。非常高兴使用这个,而不是那个丑陋的GC hack。谢谢! - Jason Tyler
如果您在多线程环境中使用此解决方案,则OpenCommands列表应为[ThreadStatic]。 - Bero

5

使用 GC.WaitForPendingFinalizers()

示例:

Con.Close();  
GC.Collect();`
GC.WaitForPendingFinalizers();
File.Delete(Environment.CurrentDirectory + "\\DATABASENAME.DB");

5

尝试这个... 这个尝试了上述代码... 对我有用

    Reader.Close()
    connection.Close()
    GC.Collect()
    GC.WaitForPendingFinalizers()
    command.Dispose()
    SQLite.SQLiteConnection.ClearAllPools()

希望这可以帮到您。

1
WaitForPendingFinalizers 对我来说真的很重要。 - Kind Contributor

4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接