I am trying to test a simple NHibernate-based auditing mechanism that stores one row per changed property into a changelog table. What it actually does, is perform the actual insert statement as expected and perform the audit logging twice.
So, this is what I do:
string connectionString = #"Data Source=.\SQLEXPRESS;Initial Catalog=audittest;Integrated Security=SSPI;";
FluentConfiguration config = Fluently.Configure().Database(MsSqlConfiguration.MsSql2008
.ConnectionString(c => c.Is(connectionString)).ShowSql())
.Mappings(x => x.FluentMappings.Add<Class1ClassMap>())
.Mappings(x => x.FluentMappings.Add<ChangeLogMap>())
.ExposeConfiguration(cfg =>
{
NHibernateAuditListener listener = new NHibernateAuditListener();
cfg.AppendListeners(ListenerType.PostInsert, new[] { listener });
});
ISessionFactory sf = config.BuildSessionFactory();
ISession session = sf.OpenSession();
using (ITransaction tr = session.BeginTransaction())
{
session.Save(new Class1()
{
FirstName="Peter",
LastName="Pan",
Id=100
});
tr.Commit();
}
EDIT:
Altered the logging code to something simple to see the failure:
public void OnPostInsert(PostInsertEvent #event)
{
if (#event.Entity is IAuditable)
{
Console.WriteLine("----write audit----");
for (int index = 0; index < #event.State.Length; index++)
Console.WriteLine("----store changes of property {0}----",
#event.Persister.PropertyNames[index]);
}
}
This generates the following output:
NHibernate: INSERT INTO "Class1" (FirstName, LastName, Id) VALUES (#p0, #p1, #p2); #p0 = 'Peter' [Type: String (0)], #p1 = 'Pan' [Type: String (0)], #p2 = 1 [Type: Int64 (0)]
----write audit----
----store changes of property FirstName----
----store changes of property LastName----
----write audit----
----store changes of property FirstName----
----store changes of property LastName----
As you see, it's not the EventHandler code that's erroneous, but the framework calling it that behaves unexpectedly (calling the OnPostInsert method twice). Any ideas why this is happening?
SAMPLE PROJECT DOWNLOAD
Okay everybody, the problem exists in the detail of the handling within the programm. You are building up a FluentConfiguration-instance which on the fly creates the the basic NHibernate configuration.
This is done on the call of these 2 lines (variable config is of type FluentConfiguration):
new SchemaExport(config.BuildConfiguration()).Create(true, true);
and
ISessionFactory sf = config.BuildSessionFactory();
The FluentConfiguration caches the first created instance and reuses it for creating the new instance for the ISessionFactory-instance. On both calls the ExposeConfiguration of FluentConfiguration instance is called. So there are 2 instances of the NHibernateAuditListener within the session that is persisting the data.
Try it like this:
string connectionString = #"Data Source=.\SQLEXPRESS;Initial Catalog=audittest;Integrated Security=SSPI;";
var config = Fluently.Configure().Database(MsSqlConfiguration.MsSql2008
.ConnectionString(c => c.Is(connectionString)).ShowSql())
.Mappings(x => x.FluentMappings.Add<Class1ClassMap>())
.Mappings(x => x.FluentMappings.Add<ChangeLogMap>())
.ExposeConfiguration(cfg =>
{
NHibernateAuditListener listener = new NHibernateAuditListener();
cfg.AppendListeners(ListenerType.PostInsert, new[] { listener });
})
.BuildConfiguration();
new SchemaExport(config).Create(true, true);
Console.WriteLine("----------------------------------------------");
ISessionFactory sf = config.BuildSessionFactory();
ISession session = sf.OpenSession();
using (ITransaction tr = session.BeginTransaction())
{
session.Save(new Class1()
{
FirstName="Peter",
LastName="Pan",
Id=100
});
tr.Commit();
}
Within config you have now the real NHibernate Configuration instance, with only one Listener registered.
Got it?!
Related
I have two entities bound as one-to-one via foreignkey: CreateTenantDto and SaasTenantCreateDto.
I need to use TWO repositories (_abpTenantRepository is an instance of 3rd party repository from ABP Framework) to insert those entities into DB. I am trying to use ABP UnitOfWork implementation for this. After SaasTenantCreateDto entity is inserted, I am trying to insert CreateTenantDto entry which depends on it. If I use OnCompleted event to insert a CreateTenantDto record - the method does not enter OnCompleted before returning newTenantDto and the latter is returned as a null (the records are inserted finally, but I want to return the inserted entity if it's inserted successfully). If I don't use OnCompleted at all - the method hangs (looks like DB lock). If I use two nested UnitOfWork objects - the method hangs as well. If I use the scope for working with two repositories -
using (var scope = ServiceProvider.CreateScope())
{
var unitOfWorkManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
using (var tenantUow = unitOfWorkManager.Begin(new AbpUnitOfWorkOptions { IsTransactional = true }))
{ ... }
}
it hangs also... It is definitely the lock and it has to do with accessing the id from the newly created newAbpTenant: I can see that in SQL Developer Sessions
enq: TX - row lock contention
and guilty session is another my HttpApi host session. Probably, the reason is as Oracle doc says: "INSERT, UPDATE, and DELETE statements on the child table do not acquire any locks on the parent table, although INSERT and UPDATE statements wait for a row-lock on the index of the parent table to clear." - SaveChangesAsync causes new record row lock?
How to resolve this issue?
//OnModelCreatingBinding
builder.Entity<Tenant>()
.HasOne(x => x.AbpTenant)
.WithOne()
.HasPrincipalKey<Volo.Saas.Tenant>(x => x.Id)
.HasForeignKey<Tenant>(x => x.AbpId);
...
b.Property(x => x.AbpId).HasColumnName("C_ABP_TENANT").IsRequired();
//Mapping ignoration to avoid problems with 'bound' entities, since using separate repositories for Insert / Update
CreateMap<CreateTenantDto, Tenant>().ForMember(x => x.AbpTenant, opt => opt.Ignore());
CreateMap<UpdateTenantDto, Tenant>().ForMember(x => x.AbpTenant, opt => opt.Ignore());
public class CreateTenantDto
{
[Required]
public int Id { get; set; }
...
public Guid? AbpId { get; set; }
public SaasTenantCreateDto AbpTenant { get; set; }
}
public async Task<TenantDto> CreateAsync(CreateTenantDto input)
{
try
{
TenantDto newTenantDto = null;
using (var uow = _unitOfWorkManager.Begin(new AbpUnitOfWorkOptions { IsTransactional = true, IsolationLevel = System.Data.IsolationLevel.Serializable }))
{
var abpTenant = await _abpTenantManager.CreateAsync(input.AbpTenant.Name, input.AbpTenant.EditionId);
input.AbpTenant.MapExtraPropertiesTo(abpTenant);
var newAbpTenant = await _abpTenantRepository.InsertAsync(abpTenant);
await uow.SaveChangesAsync();
var tenant = ObjectMapper.Map<CreateTenantDto, Tenant>(input);
tenant.AbpId = newAbpTenant.Id;
var newTenant = await _tenantRepository.InsertAsync(tenant);
newTenantDto = ObjectMapper.Map<Tenant, TenantDto>(newTenant);
await uow.CompleteAsync();
}
return newTenantDto;
}
//Implementation by ABP Framework
public virtual async Task CompleteAsync(CancellationToken cancellationToken = default)
{
if (_isRolledback)
{
return;
}
PreventMultipleComplete();
try
{
_isCompleting = true;
await SaveChangesAsync(cancellationToken);
await CommitTransactionsAsync();
IsCompleted = true;
await OnCompletedAsync();
}
catch (Exception ex)
{
_exception = ex;
throw;
}
}
I have finally resolved the problem using the following approach (but it is not using TWO repositories which seems to be impossible to implement, since we need to manipulate DbContext directly):
Application service layer:
//requiresNew: true - to be able to use TransactionScope
//isTransactional: false, otherwise it won't be possible to use TransactionScope, since we would have active ambient transaction
using var uow = _unitOfWorkManager.Begin(requiresNew: true);
var abpTenant = await _abpTenantManager.CreateAsync(input.AbpTenant.Name, input.AbpTenant.EditionId);
input.AbpTenant.MapExtraPropertiesTo(abpTenant);
var tenant = ObjectMapper.Map<CreateTenantDto, Tenant>(input);
var newTenant = await _tenantRepository.InsertAsync(tenant, abpTenant);
await uow.CompleteAsync();
return ObjectMapper.Map<Tenant, TenantDto>(newTenant);
Handmade InsertAsync method on Repository (EntityFrameworkCore) layer:
using (new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
{
var newAbpTenant = DbContext.AbpTenants.Add(abpTenant).Entity;
tenant.AbpId = newAbpTenant.Id;
var newTenant = DbContext.Tenants.Add(tenant).Entity;
if (autoSave)
{
await DbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
}
return newTenant;
}
I'm using Entity Framework Core 2.1.4 with Oracle 11 database and Devart.Data.Oracle.EFCore provider.
Database first approach.
I want to get from sequence value for ID column (primary key) on inserting without setting this explicitly every time.
So, based on similar infos with SQL Server, I did it as following:
Entity
public class Foo
{
public int Id { get; set; }
public double Value { get; set; }
}
Mapping (OnModelCreating method)
modelBuilder.HasSequence<int>("SEQ_FOOS", schema: "SCHEMA")
.StartsAt(1)
.IncrementsBy(1);
modelBuilder.Entity<Foo>(entity =>
{
entity.ForOracleToTable("FOOS");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).ForOracleHasColumnName("ID").IsRequired().ForOracleHasDefaultValueSql("SELECT SEQ_FOO.NEXTVAL FROM DUAL");
entity.Property(e => e.Value).HasColumnName("VALUE");
});
Adding value:
using (var dbContext = new FooDbContext())
{
var foo = new Foo()
{
Value = 5
};
dbContext.Foos.Add(foo);
dbContext.SaveChanges();
}
On SaveChanges:
OracleException: ORA-01400: cannot insert NULL into ("SCHEMA"."FOOS"."ID")
I also logged EF query. As you can see, there is no ID column in insert:
INSERT INTO SCHEMA.FOOS (VALUE)
VALUES (:p0)
I was trying to use simply SEQ_FOO.NEXTVAL instead of full select or default EF methods (like HasDefaultValueSql) but nothing worked. Even if I type:
ForOracleHasDefaultValueSql("asdasd");
There is no errors with this - only the same exception as above. It seems like EF never call that SQL.
Am I missing something important? Or maybe it's internal Devart problem?
Ok, I have solution. It seems we need to use ValueGenerator. My implementation below.
Mapping
entity.Property(e => e.Id)
.ForOracleHasColumnName("ID")
.IsRequired()
.ValueGeneratedOnAdd()
.HasValueGenerator((_, __) => new SequenceValueGenerator(_defaultSchema, "SEQ_FOOS"));
SequenceValueGenerator (please note that ValueGenerator is EF Core type)
internal class SequenceValueGenerator : ValueGenerator<int>
{
private string _schema;
private string _sequenceName;
public SequenceValueGenerator(string schema, string sequenceName)
{
_schema = schema;
_sequenceName = sequenceName;
}
public override bool GeneratesTemporaryValues => false;
public override int Next(EntityEntry entry)
{
using (var command = entry.Context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = $"SELECT {_schema}.{_sequenceName}.NEXTVAL FROM DUAL";
entry.Context.Database.OpenConnection();
using (var reader = command.ExecuteReader())
{
reader.Read();
return reader.GetInt32(0);
}
}
}
}
It seems to work as I needed.
Mapping:
private void FooMapping(ModelBuilder modelBuilder)
{
//modelBuilder.HasSequence<int>("SEQ_FOOS", schema: "SCHEMA")
// .StartsAt(1)
// .IncrementsBy(1);
modelBuilder.Entity<Foo>(entity =>
{
entity.ForOracleToTable("FOOS");
entity.HasKey(e => e.Id);
//entity.Property(e => e.Id).ForOracleHasColumnName("ID").IsRequired().ForOracleHasDefaultValueSql("SELECT SEQ_FOO.NEXTVAL FROM DUAL");
entity.Property(e => e.Value).HasColumnName("VALUE");
});
}
Code:
// https://www.devart.com/dotconnect/oracle/docs/?dbmonitor.html
var monitor = new OracleMonitor() { IsActive = true };
using (var dbContext = new FooModel())
{
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
var foo = new Foo()
{
Value = 5
};
dbContext.Foos.Add(foo);
dbContext.SaveChanges();
}
Check SQL generated in dbMonitor. Is that what you need?
Did not figure this out. I have similar problem on Oracle 18C - I need to fill PK in the table, PK is a NUMBER, not IDENTITY (this is obviously a defect and will be changed later on, but now I have to deal with that since I don't have rights to change DB structure, however I need to prepare CRUD demo). I don't want to use some C# value generator, but instead - DB remedy. So I tried to use the following (but it did not work - the expression is ignored):
b.HasKey(x => x.Id);
b.Property(x => x.Id).HasColumnName("C_LICENCE").IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("select round(dbms_random.value(100000, 999999)) from dual");
I suspect it's probably because int primary column is never null :) But anyway, I need to somehow force it to be generated via SQL always.
Working on a WebApi project that's backed by mssql with EntityFramework, and also Oracle (12c) using oracle's ManagedDataAccess.Client.OracleConnection. We use autofac to inject an instance of our context per request, but all oracle access is just done ad hoc.
We have certain operations that depend on both databases at the same time, so we opted to use the TransactionScope object to manage the transaction.
For the most part it works well, the light weight transactions that are promoted to distributed work great. But there is one issue I've encountered after completing a distributed transaction.
Given:
public void Test()
{
var preItem = new HelpItem
{
Field1 = "pre batch";
};
_context.Items.Add(preItem);
_context.SaveChanges(); // This save always works.
var batchResult = FooService.BatchOperation(true);
var postItem = new HelpItem
{
Field1 = "post batch";
};
_context.Items.Add(postItem);
_context.SaveChanges(); // This will succeed/fail depending on whether FooService caused a distributed transaction.
}
With the BatchOperation method as:
public Result BatchOperation(bool triggerDtc)
{
using (var transaction = new new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
if (triggerDtc){
// Make requests to both databases.
} else {
// Make request to one database.
}
// Always complete for the sake of the demonstration.
transaction.Complete();
}
}
If a distributed transaction is encountered and then completed & fully disposed EF doesn't seem to be able to recover and go back to working as it was before the transaction came into play.
The error:
Distributed transaction completed. Either enlist this session in a new
transaction or the NULL transaction.
What would be the correct way to handle this?
For this particular case you can simply create another transaction around the second part:
var batchResult = FooService.BatchOperation(true);
using (var transaction = new new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
var postItem = new HelpItem
{
Field1 = "post batch";
};
_context.Items.Add(postItem);
_context.SaveChanges(); // This save depends on whether FooService caused a distributed transaction.
transaction.Complete();
}
But this issue came up because the FooService.BatchOperation method was altered with just a lookup to the other database, unknowingly breaking every method out there that continues to use the context after calling it. With normal transaction a single EF context can freely be used in and out of them without issue, is there any way to achieve the same with a distributed transaction?
EDIT:
This really just has me confused now. Just the act of making a request in another (non distributed) transactionscope is enough to restore EF functionality.
public IHttpActionResult Test()
{
var preItem = new HelpItem
{
Field1 = "pre batch";
};
_context.Items.Add(preItem);
_context.SaveChanges(); // This save works.
var batchResult = FooService.BatchOperation(true);
using (var transaction = new new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
var lookupAnything = _context.Items.ToList();
transaction.Complete(); // This is optional, because we really don't care and it's disposed either way.
}
var postItem = new HelpItem
{
Field1 = "post batch";
};
_context.Items.Add(postItem);
_context.SaveChanges(); // Now this always works.
}
Obviously I can't just go around putting this everywhere, so still not sure what the actual solution is.
Appearently, I got an error if using the following code. It said:
Cannot implicity converrt type System.Linq.IQueryable<AnonymousType> to System.Collection.Generic.IEnumerable.
Please advise how I can fix this?
public IEnumerable<Session> GetAllListDetailConsumer(string refId)
{
ObjectQuery<Session> sessions = db.Sessions;
ObjectQuery<SessionsList> sessionsLists = db.SessionsList;
var query =
from s in sessions
join sList in sessionsLists on s.ReferralListID equals sList.ReferralListID
where s.ReferralListID == new Guid(refId)
select new SessionConsumerList
{
ReferralListID = s.ReferralListID,
SessionServerId = s.SessionServerID,
ApplicationID = s.ApplicationID,
// ...
ConsumerID = sList.ConsumerID,
ConsumerFirstName = sList.ConsumerFirstName,
ConsumerFamilyName = sList.ConsumerFamilyName,
// ...
};
return query.ToList();
}
You are selecting using select new, which would create an anonymous type, you need to project to class Session in your query like.
select new Session
{
....
But remember if your Session class is a representing a table in your database/data context, then you can't project to that class, instead you may have to create a temporary class and project the selection to that class.
EDIT (Since the question now has been edited)
Now you are selecting new SessionConsumerList and you are returning IEnumerable<Session>, you need to modify method signature to return IEnumerable<SessionConsumerList>
Why not separate the creation of the SessionConsumerList in another method? Makes the code a lot cleaner. Like this:
public static SessionConsumerList CreateSessionConsumerList(
Session s,
SessionsList sList)
{
return new SessionConsumerList
{
ReferralListID = s.ReferralListID,
SessionServerId = s.SessionServerID,
ApplicationID = s.ApplicationID,
// ...
ConsumerID = sList.ConsumerID,
ConsumerFirstName = sList.ConsumerFirstName,
ConsumerFamilyName = sList.ConsumerFamilyName,
// ...
};
}
And then:
var query =
from s in sessions
join sList in sessionsLists on s.ReferralListID equals sList.ReferralListID
where s.ReferralListID == new Guid(refId)
select CreateSessionConsumerList(s, sList);
Note: I'm specifically not using Fluent NHibernate but am using 3.x's built-in mapping style. However, I am getting a blank recordset when I think I should be getting records returned.
I'm sure I'm doing something wrong and it's driving me up a wall. :)
Background / Setup
I have an Oracle 11g database for a product by IBM called Maximo
This product has a table called workorder which lists workorders; that table has a field called "wonum" which represents a unique work order number.
I have a "reporting" user which can access the table via the maximo schema
e.g. "select * from maximo.workorder"
I am using Oracle's Managed ODP.NET DLL to accomplish data tasks, and using it for the first time.
Things I've Tried
I created a basic console application to test this
I added the OracleManagedClientDriver.cs from the NHibernate.Driver on the master branch (it is not officially in the release I'm using).
I created a POCO called WorkorderBriefBrief, which only has a WorkorderNumber field.
I created a class map, WorkorderBriefBriefMap, which maps only that value as a read-only value.
I created a console application with console output to attempt to write the lines of work orders.
The session and transaction appear to open correct,
I tested a standard ODP.NET OracleConnection to my connection string
The Code
POCO: WorkorderBriefBrief.cs
namespace PEApps.Model.WorkorderQuery
{
public class WorkorderBriefBrief
{
public virtual string WorkorderNumber { get; set; }
}
}
Mapping: WorkorderBriefBriefMap.cs
using NHibernate.Mapping.ByCode;
using NHibernate.Mapping.ByCode.Conformist;
using PEApps.Model.WorkorderQuery;
namespace ConsoleTests
{
public class WorkorderBriefBriefMap : ClassMapping<WorkorderBriefBrief>
{
public WorkorderBriefBriefMap()
{
Schema("MAXIMO");
Table("WORKORDER");
Property(x=>x.WorkorderNumber, m =>
{
m.Access(Accessor.ReadOnly);
m.Column("WONUM");
});
}
}
}
Putting it Together: Program.cs
namespace ConsoleTests
{
class Program
{
static void Main(string[] args)
{
NHibernateProfiler.Initialize();
try
{
var cfg = new Configuration();
cfg
.DataBaseIntegration(db =>
{
db.ConnectionString = "[Redacted]";
db.Dialect<Oracle10gDialect>();
db.Driver<OracleManagedDataClientDriver>();
db.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
db.BatchSize = 500;
db.LogSqlInConsole = true;
})
.AddAssembly(typeof(WorkorderBriefBriefMap).Assembly)
.SessionFactory().GenerateStatistics();
var factory = cfg.BuildSessionFactory();
List<WorkorderBriefBrief> query;
using (var session = factory.OpenSession())
{
Console.WriteLine("session opened");
Console.ReadLine();
using (var transaction = session.BeginTransaction())
{
Console.WriteLine("transaction opened");
Console.ReadLine();
query =
(from workorderbriefbrief in session.Query<WorkorderBriefBrief>() select workorderbriefbrief)
.ToList();
transaction.Commit();
Console.WriteLine("Transaction Committed");
}
}
Console.WriteLine("result length is {0}", query.Count);
Console.WriteLine("about to write WOs");
foreach (WorkorderBriefBrief wo in query)
{
Console.WriteLine("{0}", wo.WorkorderNumber);
}
Console.WriteLine("DONE!");
Console.ReadLine();
// Test a standard connection below
string constr = "[Redacted]";
OracleConnection con = new OracleConnection(constr);
con.Open();
Console.WriteLine("Connected to Oracle Database {0}, {1}", con.ServerVersion, con.DatabaseName.ToString());
con.Dispose();
Console.WriteLine("Press RETURN to exit.");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("Error : {0}", ex);
Console.ReadLine();
}
}
}
}
Thanks in advance for any help you can give!
Update
The following code (standard ADO.NET with OracleDataReader) works fine, returning the 16 workorder numbers that it should. To me, this points to my use of NHibernate more than the Oracle Managed ODP.NET. So I'm hoping it's just something stupid that I did above in the mapping or configuration.
// Test a standard connection below
string constr = "[Redacted]";
OracleConnection con = new Oracle.ManagedDataAccess.Client.OracleConnection(constr);
con.Open();
Console.WriteLine("Connected to Oracle Database {0}, {1}", con.ServerVersion, con.DatabaseName);
var cmd = new OracleCommand();
cmd.Connection = con;
cmd.CommandText = "select wonum from maximo.workorder where upper(reportedby) = 'MAXADMIN'";
cmd.CommandType = CommandType.Text;
Oracle.ManagedDataAccess.Client.OracleDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(reader.GetString(0));
}
con.Dispose();
When configuring NHibernate, you need to tell it about your mappings.
I found the answer -- thanks to Oskar's initial suggestion, I realized it wasn't just that I hadn't added the assembly, I also needed to create a new mapper.
to do this, I added the following code to the configuration before building my session factory:
var mapper = new ModelMapper();
//define mappingType(s) -- could be an array; in my case it was just 1
var mappingType = typeof (WorkorderBriefBriefMap);
//use AddMappings instead if you're mapping an array
mapper.AddMapping(mappingType);
//add the compiled results of the mapper to the configuration
cfg.AddMapping(mapper.CompileMappingForAllExplicitlyAddedEntities());
var factory = cfg.BuildSessionFactory();