I have these two operations:
public class Car {
...
public void delete() throws SQLException {
Connection c = Db.getConnection();
c.setAutoCommit(false);
if (c.getTransactionIsolation() == 0) {
c.setTransactionIsolation(c.TRANSACTION_READ_COMMITTED);
}
String sql = "DELETE FROM cars WHERE id = ?";
try(PreparedStatement s = c.prepareStatement(sql)) {
s.setInt(1, id);
s.executeUpdate();
c.commit();
}
}
}
and second one:
public class CarTransfer {
public static boolean transfer(int person_id, int car_id, int other_shop_id) throws SQLException, Exception {
Car car = FindCar.getInstance().findById(car_id);
Person person = FindPerson.getInstance().findById(person_id);
try {
if (car == null) {
throw new CallException("Car doesn't exist");
}
} catch (CallException e) {
System.out.println(e.getMessage());
}
Connection c = Db.getConnection();
c.setAutoCommit(false);
if (c.getTransactionIsolation() == 0) {
c.setTransactionIsolation(c.TRANSACTION_READ_COMMITTED);
}
String sql = "";
try {
sql = "UPDATE car_belongs_shop SET shop_id = "+other_shop_id+" WHERE car_id = "+car.getId();
} catch (NullPointerException e) {
System.out.println("Did not find a car / shop");
return false;
}
try(PreparedStatement s = c.prepareStatement(sql)) {
s.executeUpdate();
try {
if (person.getCredit() < 100) {
c.rollback();
throw new CallException("Not enough credit");
}
else {
if (car == null) {
c.rollback();
throw new CallException("Car doesn't exist");
}
else {
c.commit();
person.buy(100);
}
}
} catch (CallException | NullPointerException e) {
System.out.println(e.toString());
return false;
}
}
c.setAutoCommit(true);
return true;
}
}
So what I want to do is to transfer a car from one shop to another. But in that time, some other transaction can be done on the other side, when someone removes that car from database (that's first method delete()). What I want to do is to block any delete() method in the time when transfer is running. I'm trying to do that by this code, and it's transaction isolation (in level read committed). However, this does not work as intended, because it's still possible to remove a car whilst transfer method is running. Can you help me, whether I use sufficient isolation level or have whole transactions in the right place of the code?
Related
We wrote our own simple execution strategy to retry saving any data using our DbContext when it runs into a table lock timeout.
public class RetryTransactionExecutionStrategy : DbExecutionStrategy
{
public RetryTransactionExecutionStrategy() : base()
{
}
protected override bool ShouldRetryOn(Exception exception)
{
while (exception != null)
{
if (exception is MySqlException ex
&& ex.Number == 1205) // Deadlock error code
{
return true;
}
exception = exception.InnerException;
}
return false;
}
}
We register it by using the DbConfig class, in the same folder as the context class.
public class DbConfig : DbConfiguration
{
public DbConfig()
{
SetExecutionStrategy(MySqlProviderInvariantName.ProviderName, () => new RetryTransactionExecutionStrategy());
}
}
Now most regular usage of the context will use the retry execution strategy. However, transactions are a more special case. Microsoft mentions usage of them in their documentation, and tells the user to manually call the execution strategy, like this:
var executionStrategy = new RetryTransactionExecutionStrategy();
executionStrategy.Execute(() =>
{
using (PigDbAccountEntities pigDbAccountEntities = new PigDbAccountEntities())
{
using (var dbtransaction = pigDbAccountEntities.Database.BeginTransaction())
{
try
{
//work on some data
pigDbAccountEntities.SaveChanges();
//work on some more data
pigDbAccountEntities.SaveChanges();
//work on even more data
pigDbAccountEntities.SaveChanges();
dbtransaction.Commit();
isSaved = true;
}
catch (Exception ex)
{
dbtransaction.Rollback();
Logger.Instance.Log(LogLevel.ERROR, LogSource.DB, "error in AccountEntityManager.SaveApplicationUser", ex);
}
}
}
});
And yet we still get this error message:
The configured execution strategy 'RetryTransactionExecutionStrategy' does not support user initiated transactions. See http://go.microsoft.com/fwlink/?LinkId=309381 for additional information.
Any idea on what to do/check?
Why I'm having the fetch size ignored, I set it to 2, but whenever I run the program it returns whole records! If I print the fetch size will print 2, but the result will return whole records. Any implementation that I can return rows according to the fetch size? i.e I have 10 records in my DB I need to return 2 records for each trip in my tableAsStream method?
private Stream<SecurityGroup> tableAsStream(Context context, Connection connection) throws SQLException {
try {
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
statement.setFetchSize(FETCH_SIZE);
ResultSet resultSet = statement.executeQuery(SELECT_SCHEDULE_MODULES_QUERY);
log.info("Returning a stream of SecurityGroups from the prepared statement ");
return StreamSupport.stream(new Spliterators.AbstractSpliterator<SecurityGroup>(Long.MAX_VALUE, Spliterator.ORDERED) {
#Override
public boolean tryAdvance(Consumer<? super SecurityGroup> action) {
try {
if(!resultSet.next()) return false;
action.accept(createRecord(resultSet));
return true;
} catch(SQLException ex) {
throw new RuntimeException(ex);
}
}
}, false);
} catch(SQLException sqlEx) {
throw sqlEx;
}
}
public void migrate(Context context) throws Exception {
Connection connection = context.getConnection();
// do{
log.info("Migration script started for (SCHEDULE_SHIFTS_EDIT_SELF).");
List<SecurityGroup> securityGroupList = tableAsStream(context, connection).collect(Collectors.toList());
securityGroupList.stream()
.flatMap(securityGroup -> securityGroup.getModules().stream())
.filter(securityModule -> securityModule.getName() == ModuleName.SCHEDULE)
.forEach(filteredSecurityModule -> {
boolean editPermissionExists = filteredSecurityModule.getFeatures().stream()
.anyMatch(x->PermissionName.SCHEDULE_SHIFTS_EDIT == x.getName());
boolean editSelfPermissionExists = filteredSecurityModule.getFeatures().stream()
.anyMatch(x->PermissionName.SCHEDULE_SHIFTS_EDIT_SELF == x.getName());
if (editPermissionExists && !editSelfPermissionExists) {
filteredSecurityModule.getFeatures().add(SecurityFeature.of(PermissionName.SCHEDULE_SHIFTS_EDIT_SELF, true, false));
}
});
updateSecurityGroups(securityGroupList, context);
log.info("Migration script Ended for (SCHEDULE_SHIFTS_EDIT_SELF).");
// } while(condition);
}
I am having a problem with repeating previous operations when there is an error in the SaveChanges method of Entity Framework.
Below is the code block
public static int SaveChangesTask(this DbContext db)
{
int result = -1;int countLoop = 0;
bool continueLoop = true;
var modifiedOrAddedEntities = db.ChangeTracker.Entries().Where(a => a.State != EntityState.Detached
&& a.State != EntityState.Unchanged).ToList();
while (continueLoop && countLoop<3)
{
try
{
result= db.SaveChanges();
continueLoop = false;
}
catch(Exception ex)
{
string error = ex.ToSystemException();
if(error.ToLowerInvariant().Contains("ORA-00060".ToLowerInvariant()) || error.ToLowerInvariant().Contains("deadlock"))
{
foreach (var item in modifiedOrAddedEntities)
{
db.Entry(item).State = item.State;
}
countLoop++;
Random rnd = new Random();
System.Threading.Thread.Sleep(rnd.Next(1, 5)* 1000);
}
else
{
throw ex;
}
}
}
return result;
}
But when I want to add old tracking objects to context, Entity Framework Throws Exception like that
"The entity type DbEntityEntry is not part of the model for the current context"
I'm currently working on a generic function for inserting data tables via entity framework. However, with my current solution I ended up with a ton of very repetitive code with only a few minor differences. I would like to simplify what I have and remove the need for large case statements based on my table names (I only included two cases in this example to save space).
Here is what I currently have:
public static void InsertByTable(IEnumerable<DataTable> chunkedTable, string tableName)
{
switch (tableName)
{
#region Parcel
case TaxDataConstant.Parcel:
Parallel.ForEach(
chunkedTable,
new ParallelOptions
{
MaxDegreeOfParallelism = Convert.ToInt32(ConfigurationManager.AppSettings["MaxThreads"])
},
chunk =>
{
Realty_Records_ProdEntities entities = null;
try
{
entities = new Realty_Records_ProdEntities();
entities.Configuration.AutoDetectChangesEnabled = false;
foreach (DataRow dr in chunk.Rows)
{
var parcelToInsert = new Parcel();
foreach (DataColumn c in dr.Table.Columns)
{
SetProperty(parcelToInsert, c.ColumnName, dr[c.ColumnName]);
}
entities.Parcels.Add(parcelToInsert);
}
entities.SaveChanges();
}
catch (Exception ex)
{
TaxDataError.AddTaxApplicationLog(
TaxDataConstant.CategoryError,
ex.Source,
ex.Message,
ex.StackTrace);
throw;
}
finally
{
entities?.Dispose();
}
});
break;
#endregion
#region Asmt
case TaxDataConstant.Asmt:
Parallel.ForEach(
chunkedTable,
new ParallelOptions
{
MaxDegreeOfParallelism = Convert.ToInt32(ConfigurationManager.AppSettings["MaxThreads"])
},
chunk =>
{
Realty_Records_ProdEntities entities = null;
try
{
entities = new Realty_Records_ProdEntities();
entities.Configuration.AutoDetectChangesEnabled = false;
foreach (DataRow dr in chunk.Rows)
{
var asmtToInsert = new Asmt();
foreach (DataColumn c in dr.Table.Columns)
{
SetProperty(asmtToInsert, c.ColumnName, dr[c.ColumnName]);
}
entities.Asmts.Add(asmtToInsert);
}
entities.SaveChanges();
}
catch (Exception ex)
{
TaxDataError.AddTaxApplicationLog(
TaxDataConstant.CategoryError,
ex.Source,
ex.Message,
ex.StackTrace);
throw;
}
finally
{
entities?.Dispose();
}
});
break;
#endregion
}
}
Is there any way I can make this table agnostic?
You can probably move the logic in the case statement to a helper extension method which is generic. something like this (untested) pseudo code:
public static class EntityAdder
{
public static void AddEntities<T>(this IEnumerable<DataTable> chunkedEntities, Action<Realty_Records_ProdEntities, T entity> addingFunction)
{
Parallel.ForEach(
chunkedTable,
new ParallelOptions
{
MaxDegreeOfParallelism = Convert.ToInt32(ConfigurationManager.AppSettings["MaxThreads"])
},
chunk =>
{
Realty_Records_ProdEntities entities = null;
try
{
entities = new Realty_Records_ProdEntities();
entities.Configuration.AutoDetectChangesEnabled = false;
foreach (DataRow dr in chunk.Rows)
{
var toInsert = new T();
foreach (DataColumn c in dr.Table.Columns)
{
SetProperty(parcelToInsert, c.ColumnName, dr[c.ColumnName]);
}
addingFunction(entities,toInsert);
}
entities.SaveChanges();
}
catch (Exception ex)
{
TaxDataError.AddTaxApplicationLog(
TaxDataConstant.CategoryError,
ex.Source,
ex.Message,
ex.StackTrace);
throw;
}
finally
{
entities?.Dispose();
}
});
}
}
this could then be used something like this:
public static void InsertByTable(IEnumerable<DataTable> chunkedTable, string tableName)
{
switch (tableName)
{
#region Parcel
case TaxDataConstant.Parcel:
chunkedTable.AddEntities((entities,newEntity)=> entities.Parcels.Add(newEntity))
break;
#endregion
#region Asmt
case TaxDataConstant.Asmt:
chunkedTable.AddEntities((entities,newEntity)=> entities.Asmts.Add(newEntity))
break;
#endregion
}
}
you might be able to automate the adding bit by finding the property on the class which is a list of the things you want to add and avoid passing the action in if you wanted.
Based on Sams response and some further research I was able to refactor out the case statement.
Here's my solution:
public static void InsertByTable(IEnumerable<DataTable> chunkedTable, Type type)
{
Parallel.ForEach(
chunkedTable,
new ParallelOptions
{
MaxDegreeOfParallelism = Convert.ToInt32(ConfigurationManager.AppSettings["MaxThreads"])
},
chunk =>
{
Realty_Records_ProdEntities entities = null;
try
{
entities = new Realty_Records_ProdEntities();
entities.Configuration.AutoDetectChangesEnabled = false;
var set = entities.Set(type);
foreach (DataRow dr in chunk.Rows)
{
var objectToInsert = Activator.CreateInstance(type);
foreach (DataColumn c in dr.Table.Columns)
{
SetProperty(objectToInsert, c.ColumnName, dr[c.ColumnName]);
}
set.Add(objectToInsert);
}
entities.SaveChanges();
}
catch (Exception ex)
{
TaxDataError.AddTaxApplicationLog(
TaxDataConstant.CategoryError,
ex.Source,
ex.Message,
ex.StackTrace);
throw;
}
finally
{
entities?.Dispose();
}
});
}
Hi I was wondering if EntityReference.Load method includes
If Not ref.IsLoaded Then ref.Load()
My question is basically:
Dim person = Context.Persons.FirstOrDefault
person.AddressReference.Load()
person.AddressReference.Load() 'Does it do anything?
It does Load again. I verified this by Profiler and it shown two queries. Default merge option is MergeOption.AppendOnly and it doesn't prevent from querying again. Code from Reflector:
public override void Load(MergeOption mergeOption)
{
base.CheckOwnerNull();
ObjectQuery<TEntity> query = base.ValidateLoad<TEntity>(mergeOption, "EntityReference");
base._suppressEvents = true;
try
{
List<TEntity> collection = new List<TEntity>(RelatedEnd.GetResults<TEntity>(query));
if (collection.Count > 1)
{
throw EntityUtil.MoreThanExpectedRelatedEntitiesFound();
}
if (collection.Count == 0)
{
if (base.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One)
{
throw EntityUtil.LessThanExpectedRelatedEntitiesFound();
}
if ((mergeOption == MergeOption.OverwriteChanges) || (mergeOption == MergeOption.PreserveChanges))
{
EntityKey entityKey = ObjectStateManager.FindKeyOnEntityWithRelationships(base.Owner);
EntityUtil.CheckEntityKeyNull(entityKey);
ObjectStateManager.RemoveRelationships(base.ObjectContext, mergeOption, (AssociationSet) base.RelationshipSet, entityKey, (AssociationEndMember) base.FromEndProperty);
}
base._isLoaded = true;
}
else
{
base.Merge<TEntity>(collection, mergeOption, true);
}
}
finally
{
base._suppressEvents = false;
}
this.OnAssociationChanged(CollectionChangeAction.Refresh, null);
}
Just for reference for anyone else finding the accepted answer, here is the extension method I created for my current project.
using System.Data.Objects.DataClasses;
namespace ProjectName
{
public static class EntityFrameworkExtensions
{
public static void EnsureLoaded<TEntity>(this EntityReference<TEntity> reference)
where TEntity : class, IEntityWithRelationships
{
if (!reference.IsLoaded)
reference.Load();
}
}
}
And usage:
Patient patient = // get patient
patient.ClinicReference.EnsureLoaded();
patient.Clinic.DoStuff();