嵌套的SqlConnection.Open在TransactionScope内抛出异常

3

我在存储库单元测试中使用TransactionScope来回滚由测试所做的任何更改。

测试的设置和拆卸过程如下:

[TestFixture]
public class DeviceRepositoryTests {
    private static readonly string ConnectionString =
        ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString;

    private TransactionScope transaction;
    private DeviceRepository repository;

    [SetUp]
    public void SetUp() {
        transaction = new TransactionScope(TransactionScopeOption.Required);
        repository = new DeviceRepository(ConnectionString);
    }

    [TearDown]
    public void TearDown() {
        transaction.Dispose();
    }
}

问题测试包括将记录插入数据库的代码和检索这些记录的CUT。

    [Test]
    public async void GetAll_DeviceHasSensors_ReturnsDevicesWithSensors() {
        int device1Id = AddDevice();
        AddSensor();

        var devices = await repository.GetAllAsync();

        // Asserts
    }

AddDeviceAddSensor方法打开SQL连接并插入一行数据到数据库中:

    private int AddDevice() {
        var sqlString = "<SQL>";
        using (var connection = CreateConnection()) 
        using (var command = new SqlCommand(sqlString, connection)) {
            var insertedId = command.ExecuteScalar();
            Assert.AreNotEqual(0, insertedId);
            return (int) insertedId;
        }
    }

    private void AddSensor() {
        const string sqlString = "<SQL>";
        using (var connection = CreateConnection()) 
        using (var command = new SqlCommand(sqlString, connection)) {
            var rowsAffected = command.ExecuteNonQuery();
            Assert.AreEqual(1, rowsAffected);
        }
    }

    private SqlConnection CreateConnection() {
        var result = new SqlConnection(ConnectionString);
        result.Open();
        return result;
    }

GetAllAsync方法打开连接,执行查询,并为每个获取的行打开新的连接以获取子对象。

public class DeviceRepository {
    private readonly string connectionString;

    public DeviceRepository(string connectionString) {
        this.connectionString = connectionString;
    }

    public async Task<List<Device>> GetAllAsync() {
        var result = new List<Device>();
        const string sql = "<SQL>";

        using (var connection = await CreateConnection())
        using (var command = GetCommand(sql, connection, null))
        using (var reader = await command.ExecuteReaderAsync()) {
            while (await reader.ReadAsync()) {
                var device = new Device {
                    Id = reader.GetInt32(reader.GetOrdinal("id"))
                };

                device.Sensors = await GetSensors(device.Id);

                result.Add(device);
            }
        }

        return result;
    }

    private async Task<List<Sensor>> GetSensors(int deviceId) {
        var result = new List<Sensor>();
        const string sql = "<SQL>";

        using (var connection = await CreateConnection()) 
        using (var command = GetCommand(sql, connection, null))
        using (var reader = await command.ExecuteReaderAsync()) {
            while (await reader.ReadAsync()) {
                // Fetch row and add object to result
            }
        }

        return result;
    }

    private async Task<SqlConnection> CreateConnection() {
        var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();
        return connection;
    }
}

问题在于当GetSensors方法调用SqlConnection.Open时,会出现以下异常:

System.Transactions.TransactionAbortedException:事务已中止。
  ----> System.Transactions.TransactionPromotionException:尝试提升事务时失败。
  ----> System.Data.SqlClient.SqlException:该命令已有与此关联的打开的DataReader,必须先关闭它。
  ----> System.ComponentModel.Win32Exception:等待操作超时

我可以将获取子对象的代码移出第一个连接范围(这将起作用),但假设我不想这样做。

这个异常是否意味着在单个TransactionScope内无法同时打开对DB的连接?

编辑

GetCommand只是调用SqlCommand构造函数并进行一些日志记录。

private static SqlCommand GetCommand(string sql, SqlConnection connection, SqlParameter[] parameters) {
    LogSql(sql);
    var command = new SqlCommand(sql, connection);

    if (parameters != null)
        command.Parameters.AddRange(parameters);

    return command;
}

不行。这个消息告诉你,你只能使用一个DbCommand对象和一个DbDataReader对象。但是你正在尝试在两个DbDataReaders上使用相同的DbCommand对象。看起来问题出在你的GetCommand方法上。 - user743414
我已经更新了我的问题,包括 GetCommand 方法的主体。它非常简单,只是创建了一个 SqlCommand 对象。 - aadam
如果启用了MultipleActiveResultSets,问题是否解决?(我不知道是什么原因导致的) - Scott Chamberlain
可以这样做。但为什么?我只想从连接池中创建第二个连接,而不是在一个连接内使用多个读取器。 - aadam
1个回答

0
问题在于两个DataReader对象不能同时打开数据库(除非启用了MARS)。这是设计上的限制。我认为你有几个选择:
  1. 在连接字符串中启用MARS;添加此MultipleActiveResultSets=True
  2. 如果不是真正必要,就不要使用DataReader。但是,按照你编写代码的方式,它是相当必要的。
  3. 在加载设备后,填充Sensor属性。
  4. 使用Dapper,它可以完成所有这些操作(包括填充Sensor),并且可能更快。
使用Dapper,您可以像这样做(您将不需要GetSensors):
public async Task<List<Device>> GetAllAsync() {
    var result = new List<Device>();
    const string sql = "<SQL>";

    using (var connection = await CreateConnection())
    using (var multi = connection.QueryMultiple(sql, parms)) {
        result = multi.Read<Device>().ToList();
        var sensors = multi.Read<Sensors>().ToList();

        result.ForEach(device => device.Sensors =
            sensors.Where(s => s.DeviceId = device.Id).ToList());
    }

    return result;
}

这里你的sql语句会是这个样子:
SELECT * FROM Devices
SELECT * FROM Sensors

请参阅Dapper的多映射文档

事实上是错误的。您可以同时拥有多个活动查询... http://www.c-sharpcorner.com/uploadfile/sudhi.pro/multiple-active-result-sets-mars/ - TomTom
1
我知道尝试重用已打开的DataReader连接。但我不明白为什么。当我尝试查询传感器时,框架为什么不会打开新的连接?我以为这就是连接池的作用:当我调用new SqlConnection时,框架会在池中查找合适的(未使用、已释放)连接。如果没有,则创建一个新的并返回它。在请求查询传感器时,用于查询设备表的连接在新连接被请求之前未被释放。 - aadam
SqlConnection、SqlCommand等是MSSQL-Server的对象吗?我从未遇到过这样的问题。我可以打开许多并发的SqlConnection,并使用并发的SqlDataReaders吗?! - user743414
@user743414:请注意我的第一句话(除非启用了“MARS”)。 - Mike Perrenoud
是的,@user743414,默认情况下它没有开启。必须通过连接字符串启用MARS。 - Mike Perrenoud
显示剩余2条评论

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