How do I use a transaction when reading data with Entity Framework 4?
I'm trying to utilize the SNAPSHOT transaction isolation level in Microsoft SQL Server 2008 R2 with the Entity Framework 4.0. However, this doesn't seem to be as easy as I first thought.
To use SNAPSHOT isolation level, it has to be enabled in the database. I've done that. And I've tested by using SQL Management Studio that SNAPSHOT isolation level works as expected on my database. I want to use this isolation level because I want consistent reads without locking the rows or the whole table. So my database is ready for me to use SNAPSHOT isolation level. So far so good.
In my repro application, which is a WPF application, I have a window in which I load some data from a single table. I load 5 rows at a time every time I click a button. This is the开发者_JS百科 XAML for the window:
<Window x:Class="EFSnapshotTransactionTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" Name="UC" Closing="UC_Closing">
<DockPanel>
<Button Click="Button_Click" DockPanel.Dock="Top">Load next 5</Button>
<ScrollViewer>
<ListView ItemsSource="{Binding ElementName=UC, Path=ViewModel.Items}">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" DisplayMemberBinding="{Binding Id}"/>
<GridViewColumn Header="Date" DisplayMemberBinding="{Binding Date}"/>
<GridViewColumn Header="DocumentNumber" DisplayMemberBinding="{Binding DocumentNumber}"/>
<GridViewColumn Header="Amount" DisplayMemberBinding="{Binding Amount}"/>
<GridViewColumn Header="Text" DisplayMemberBinding="{Binding Text}"/>
</GridView>
</ListView.View>
</ListView>
</ScrollViewer>
</DockPanel>
And this is the code-behind for the window:
public partial class MainWindow : Window
{
private ViewModel _vm;
public ViewModel ViewModel
{
get { return _vm; }
}
public MainWindow()
{
_vm = new ViewModel();
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
_vm.LoadNextItems(5);
}
private void UC_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
_vm.Dispose();
}
Nothing magically going on here. Now for the code to the view model, which is where the action happens.
public class ViewModel : INotifyPropertyChanged, IDisposable
{
private ObservableCollection<Posting> _items;
private SentaFinancialsEntities _db;
private DbTransaction _dbTrans;
public ObservableCollection<Posting> Items
{
get { return _items; }
set
{
_items = value;
OnPropertyChanged("Items");
}
}
public ViewModel()
{
_items = new ObservableCollection<Posting>();
_db = new SentaFinancialsEntities();
_db.Connection.Open();
_dbTrans = _db.Connection.BeginTransaction(System.Data.IsolationLevel.Snapshot);
}
public void LoadNextItems(int count)
{
int startAt = _items.Count;
var dbPostings = (from b in _db.Postings
select b).OrderBy(b => b.Dato).Skip(startAt).Take(count);
foreach (var singleDbPosting in dbPostings)
{
Posting dto = new Posting(singleDbPosting);
_items.Add(dto);
}
}
public void Dispose()
{
_dbTrans.Commit();
_dbTrans.Dispose();
_db.Dispose();
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
What I'm trying to do here, is to open a connection to the database and keep it open. I try to start a transaction and ask for the SNAPSHOT isolation level. This would allow me to read 5 rows at a time and get the rows as they were when the window was opened, even if someone would edit, delete or insert rows while the window is open. But when I run a trace with SQL Profiler, there's no transaction being started when the window opens or when I load rows, and the isolation level I asked for is not being set. When the window opens, a connection is opened, and Entity Framework sets the transaction isolation level to READ COMMITTED which is the default isolation level. The same happens (I.e. nothing) if I use a TransactionScope instead of a DbTransaction.
So my question is: How can I start a transaction with SNAPSHOT isolation level and keep it open for as long as my window is open? It's absolutely neccessary that the transaction is kept open so I can keep reading data from the connection, without reading rows other users has added in the mean time.
I know I can do it with raw SQL commands, but I would like to avoid that if possible.
Sidenote: People has different oppinions on the different isolation levels, but this question is not for discussing whether or not SNAPSHOT isolation level is appropiate in this case. SNAPSHOT works perfectly with our business requirement for this task. The question could really be about any other isolation level as well, as other isolation levels doesn't work either with this code.
I'm sorry, I've been wasting your time. The code I posted actually works, to my surprise. I tested my program by using SQL Profiler and looked for a "BEGIN TRANSACTION" statement and a "SET TRANSACTION ISOLATION LEVEL SNAPSHOT". It turns out though, that to track transactions, you need to specifically select them in the event list in SQL Profiler. I wasn't aware of that. I thought transactions would be tracked as normal SQL commands in Profiler. Additionally, I found out that SQL Profiler cannot trace changes in transaction isolation levels. To find out what transaction isolation level a transaction is in, you have to query the sys.dm_exec_sessions system view. It has a column called "transaction_isolation_level" that has a numeric value that corresponds to an isolation level. You can see what the number means in the documentation for the view.
When I realized this, I tried my original code and queried the view, and behold! It was indeed in SNAPSHOT isolation level.
I hope this can save someone else some time. :-)
Use a TransactionOptions
to control the isolation level of the system transaction scope:
var TransactionOptions to = new TransactionOptions ()
{ IsolationLevel = IsolationLevel.Snapshot};
using (TransactionScope scope = new TransactionScope(
TransactionScope.Required, to))
{
// Do the work here
...
scope.Complete ();
}
If left unspecified, the System.Transactions will use Serializable
isolation level. You can also use an isolation level of ReadCommitted
if you enabled read_committed_snapshot in the database.
As general rules:
- is better to open a connection just for the duration an operation and close it immediately. Connection pooling will take it from there.
- is absolutely forbidden to hold a transaction for the lifetime of a form. Transaction can live only on a stack scope, for the duration of a specific operation (ie. for one button click). Otherwise Forgetful Fred will leave his form open and go to lunch, freezing the entire database with his pending transaction.
精彩评论