serenity.is master detail relation foreign key is null - serenity-platform

in serenity.is
when i try to implement master detail relation the master key is not save into the column in detail
A-master
1--row
[DisplayName("Conversation"), MasterDetailRelation(foreignKey:"ModelId"), NotMapped]
public List<LeaveRequestConversationRow> ConversationList
{
get => fields.ConversationList[this];
set => fields.ConversationList[this] = value;
}
2--form
[Category("Db.Custom.Responses")]
[LeaveRequestConversationEditor]
public List<LeaveRequestConversationRow> ConversationList { get; set; }
B-detail
1--row
[DisplayName("Model"), NotNull, Column("ModelId"), ForeignKey("[dbo].[LeaveRequest]", "Id"), LeftJoin("jLeaveRequest"), TextualField("Model")]
public Int32? ModelId
{
get => fields.ModelId[this];
set => fields.ModelId[this] = value;
}
2--editor
namespace Indotalent.Leave {
#Serenity.Decorators.registerClass()
export class LeaveRequestConversationEditor extends Serenity.Extensions.GridEditorBase<LeaveRequestConversationRow> {
protected getColumnsKey() { return LeaveRequestConversationColumns.columnsKey; }
protected getDialogType() { return LeaveRequestConversationDialog; }
protected getLocalTextPrefix() { return LeaveRequestConversationRow.localTextPrefix; }
constructor(container: JQuery) {
super(container);
}
}
}
i try to make 1 to many in master detail relation
and i expecting the id of master is stored onto detail

Related

Entity Framework Core import / export tree of entities with similar foreign keys

I need to add an import/export functionality to my ASP.NET Core application.
What I would like is to take entities in one database, export these entities into one file, and then import that file into a new database.
My problem is that I have some entities that hold same foreign key. Here is a simple model illustrating what I want to do:
public class Bill
{
public List<Product> Products { get; set; }
public int Id { get; set; }
...
}
public class Product
{
public int Id { get; set; }
public int ProductCategoryId { get; set; }
public ProductCategory ProductCategory { get; set; }
...
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
I want to export a bill to be imported on an other environment of my application. So if I export the bill, I will get a Json like this:
{
"id": 1,
"products" : [
{
"id" : 1,
"productCategoryId": 1,
"productCategory": {
"id" : 1,
"name" : "Category #1"
}
},
{
"id" : 2,
"productCategoryId": 1,
"productCategory": {
"id" : 1,
"name" : "Category #1"
}
},
{
"id" : 3,
"productCategoryId": 1,
"productCategory": {
"id" : 2,
"name" : "Category #2"
}
}
]
}
If I deserialize this json into entities in my new environment (ignoring Ids mapping of course), I will get three new categories (category will be duplicated for product 1 and 2) because the serializer will instanciate two categories...
So when I push it into my database, it will add 3 lines instead 2 into the Category table...
Thanks in advance for your answers.
Suppose you have a list of Category to be imported, you could firstly get the id list of these categories and then query the database to make sure which has been already stored in database. And for those already existing ones, just skip them (or update them as you like).
Since we have multiple entity types (categories,products,bills and potential BillProducts), rather than writing a new importer for each TEntity , I prefer to writing a generic Importer method to deal with any Entity type list with Generic and Reflection :
public async Task ImportBatch<TEntity,TKey>(IList<TEntity> entites)
where TEntity : class
where TKey: IEquatable<TKey>
{
var ids = entites.Select( e=> GetId<TEntity,TKey>(e));
var existingsInDatabase=this._context.Set<TEntity>()
.Where(e=> ids.Any(i => i.Equals(GetId<TEntity,TKey>(e)) ))
.ToList();
using (var transaction = this._context.Database.BeginTransaction())
{
try{
this._context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT " + typeof(TEntity).Name + " ON;");
this._context.SaveChanges();
foreach (var entity in entites)
{
var e= existingsInDatabase.Find(existing => {
var k1 =GetId<TEntity,TKey>(existing);
var k2=GetId<TEntity,TKey>(entity);
return k1.Equals(k2);
});
// if not yet exists
if(e == null){
this._context.Add(entity);
}else{
// if you would like to update the old one when there're some differences
// uncomment the following line :
// this._context.Entry(e).CurrentValues.SetValues(entity);
}
}
await this._context.SaveChangesAsync();
transaction.Commit();
}
catch{
transaction.Rollback();
}
finally{
this._context.Database.ExecuteSqlCommand($"SET IDENTITY_INSERT " + typeof(TEntity).Name + " OFF;");
await this._context.SaveChangesAsync();
}
}
return;
}
Here the GetId<TEntity,TKey>(TEntity e) is a simple helper method which is used to get the key filed of e:
// use reflection to get the Id of any TEntity type
private static TKey GetId<TEntity,TKey>(TEntity e)
where TEntity : class
where TKey : IEquatable<TKey>
{
PropertyInfo pi=typeof(TEntity).GetProperty("Id");
if(pi == null) { throw new Exception($"the type {typeof(TEntity)} must has a property of `Id`"); }
TKey k = (TKey) pi.GetValue(e);
return k ;
}
To make the code more reusable, we can create an EntityImporter service to hold the method above :
public class EntityImporter{
private DbContext _context;
public EntityImporter(DbContext dbContext){
this._context = dbContext;
}
public async Task ImportBatch<TEntity,TKey>(IList<TEntity> entites)
where TEntity : class
where TKey: IEquatable<TKey>
{
// ...
}
public static TKey GetId<TEntity,TKey>(TEntity e)
where TEntity : class
where TKey : IEquatable<TKey>
{
// ...
}
}
and then register the services at startup time:
services.AddScoped<DbContext, AppDbContext>();
services.AddScoped<EntityImporter>();
Test Case :
Firstly, I'll take several categories as an example :
var categories = new ProductCategory[]{
new ProductCategory{
Id = 1,
Name="Category #1"
},
new ProductCategory{
Id = 2,
Name="Category #2"
},
new ProductCategory{
Id = 3,
Name="Category #3"
},
new ProductCategory{
Id = 2,
Name="Category #2"
},
new ProductCategory{
Id = 1,
Name="Category #1"
},
};
await this._importer.ImportBatch<ProductCategory,int>(categories);
The expected results should be only 3 rows imported :
1 category #1
2 category #2
3 category #3
And here's a screenshot it works:
Finally, for your bills json, you can do as below to import the entites :
var categories = bill.Products.Select(p=>p.ProductCategory).ToList();
var products = bill.Products.ToList();
// other List<TEntity>...
// import the categories firstly , since they might be referenced by other entity
await this._importer.ImportBatch<ProductCategory,int>(categories);
// import the product secondly , since they might be referenced by BillProduct table
await this._import.ImportBatch<Product,int>(products);
// ... import others

Disable conversion between VARCHAR and NVARCHAR when using Entity Framework over an existing SQL server database

I have an existing database SQL server and I'm using EF6 over it. Let's assume I have the following code:
private sealed class Foo
{
public int Id { get; set; }
public string Bar { get; set; }
}
private sealed class FooConfiguration : EntityTypeConfiguration<Foo>
{
public FooConfiguration()
{
ToTable("Foos");
HasKey(e => e.Id);
Property(e => e.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
private class FooContext : DbContext
{
public FooContext(string connectionString)
: base(connectionString)
{
Database.SetInitializer<FooContext>(null);
}
public virtual DbSet<Foo> Foos { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.Add(new FooConfiguration());
}
}
Usage:
const string bar = "حالياً البحث عبر جوجل";
int fooId;
using (var db = new FooContext(connectionString))
{
var foo = db.Foos.Add(new Foo { Bar = bar });
db.SaveChanges();
fooId = foo.Id;
}
using (var db = new FooContext(connectionString))
{
var foo = db.Foos.First(f => f.Id == fooId);
if (foo.Bar != bar)
{
Console.WriteLine("Oops!");
}
}
What can possibly go wrong? Well, if the Bar column has type VARCHAR(MAX) instead of NVARCHAR(MAX), then we are in a bad position, because VARCHAR cannot properly store that string of Unicode characters, so we get a bunch of question marks instead.
So, the question is: can I some how disable this conversion between VARCHAR and NVARCHAR and enforce EF to throw some sort of type mismatch exception during SaveChanges? I've tried to use this in my configuration class:
Property(e => e.Bar).IsUnicode(true);
Property(e => e.Bar).HasColumnType("NVARCHAR(MAX)");
but it did nothing.
Thanks in advance.
The default data type for string columns is nvarchar(MAX), you don't have the need to set data type in your columns for changing to NVARCHAR(MAX). Ef has default conventions that Specify it. here is explained for knowing conventions.
if you want config generally convention for your models, you can do this:
public class DataTypeConvention : Convention
{
public DataTypeConvention()
{
Properties<string>().Configure(config => { config.HasColumnType("nvarchar(MAX)"); });
}
}
on your DbContext:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add(new DataTypeConvention());
}

Orchard CMS ISessionConfigurationEvents and 1:N, N:N Relationships?

I have spent many days trying to implement relationships within OrchardCMS 1.9.1 between my custom contentParts to no avail.
Strewn across the internet are many others trying to achieve the same thing, who have also failed; giving me the impression that it's impossible?
Though recently I read an article at: http://www.ideliverable.com/blog/isessionconfigurationevents that gave the impression that all things possible with Fluent Nhibernate should be possible within Orchard.
So I implemented:
public class DbMapping : ISessionConfigurationEvents
{
public void Created(FluentConfiguration cfg, AutoPersistenceModel defaultModel)
{
defaultModel.UseOverridesFromAssemblyOf<ProfilePartRecord>().Alterations(x => x.AddFromAssemblyOf<ProfileOverride>());
defaultModel.UseOverridesFromAssemblyOf<LocationPartRecord>().Alterations(x => x.AddFromAssemblyOf<LocationOverride>());
}
public void Prepared(FluentConfiguration cfg) { }
public void Building(Configuration cfg) { }
public void Finished(Configuration cfg) { }
public void ComputingHash(Hash hash) { }
}
public class LocationOverride : IAutoMappingOverride<LocationPartRecord>
{
public void Override(AutoMapping<LocationPartRecord> mapping)
{
//[ Profile ] <--> [ Location ]
//mapping.Id(x => x.Id, "LocationPartRecord_id"); //As it's not in the model due to being a contentPart, NH will throw an error because of such.
mapping.Map(x => x.Type);
mapping.Map(x => x.Name);
mapping.References(x => x.ProfilePartRecord, "ProfilePartRecord_id");
}
}
public class ProfileOverride : IAutoMappingOverride<ProfilePartRecord>
{
public void Override(AutoMapping<ProfilePartRecord> mapping)
{
//[ Profile ] 0.1 <---> N [ Location ]
//NEW
mapping.HasMany(x => x.Locations)
.Inverse()
//.KeyColumn("ProfilePartRecord_id")
.Cascade.All()
.ForeignKeyCascadeOnDelete()
.ForeignKeyConstraintName("FK_Location__Profile");
}
}
MODELS:
public class ProfilePartRecord : ContentPartRecord
{
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
[CascadeAllDeleteOrphan]
public virtual IList<LocationPartRecord> Locations { get; set; }
public ProfilePartRecord()
{
Locations = new List<LocationPartRecord>();
}
}
public class LocationPartRecord : ContentPartRecord
{
public virtual string Type { get; set; }
public virtual string Name { get; set; }
//For HasMany
[CascadeAllDeleteOrphan]
public virtual ProfilePartRecord ProfilePartRecord { get; set; }
}
MIGRATION:
SchemaBuilder.CreateTable("ProfilePartRecord",
table => table
.ContentPartRecord()
//PK: ProfilePartRecord_id
.Column<string>("FirstName")
.Column<string>("LastName")
//System
.Column<DateTime>("CreatedAt")
);
ContentDefinitionManager.AlterPartDefinition("ProfilePart",
builder => builder.Attachable());
ContentDefinitionManager.AlterTypeDefinition("Profile", t => t
.WithPart(typeof(ProfilePart).Name)
.WithPart("UserPart")
);
ContentDefinitionManager.AlterTypeDefinition("User", t => t
.WithPart("ProfilePart")
);
SchemaBuilder.CreateTable("LocationPartRecord",
table => table
.ContentPartRecord()
//PK: LocationPartRecord_id
//FK:
.Column<int>("ProfilePartRecord_id")
.Column<string>("Type")
.Column<string>("Name")
//System
.Column<DateTime>("CreatedAt")
);
ContentDefinitionManager.AlterPartDefinition("LocationPart",
builder => builder.Attachable());
ContentDefinitionManager.AlterTypeDefinition("Location", type => type
.WithPart("CommonPart")
.WithPart("LocationPart")
.Creatable()
.Listable());
But alas, I still can't create a relationship between these two entities. I can do such via Migration, but this is very limited - as in - I can't set the relationship to Cascade.
Can anyone shed some light on whether this is possible, and if so, how? Thanks
This may not get you the whole way, but I believe it will help. One thing I have done for performance reasons as well as to establish relationships at the database level between my parts is to use the "CreateForeignKey" and "CreateIndex" in the Migration. Here is an example that should work for you
// Add foreign key
SchemaBuilder.CreateForeignKey(
"FK_LocationProfile",
"LocationPartRecord", new[] { "ProfilePartRecord_id" },
"ProfilePartRecord", new[] { "Id" });
// Add index
SchemaBuilder.AlterTable("LocationPartRecord",
table => table
.CreateIndex("IDX_ProfilePartRecord_Id", "ProfilePartRecord_Id")
);
With these relationships defined, I wonder if that will in any way impact the NHibernate work you are doing.
As for how we have done the overall goal I believe you are trying to achieve, you can monitor the "ProfilePart" "Delete" event in the "LocationPart" handler and apply your own cascading delete logic there to ensure that there are no "LocationPart" left around.

How to change the collection name on an index

When I save a document that has a generic type DataView<Customer>, I'm manually setting the collection name to "customers". However, I'm having some trouble making an index using AbstractIndexCreationTask with a non-default collection name. Here's my index:
public class customers_Search
: AbstractIndexCreationTask<DataView<Customer>, customers_Search.Result>
{
public class Result
{
public string Query { get; set; }
}
public customers_Search()
{
Map = customers =>
from customer in customers
where customer.Data != null
select new
{
Query = AsDocument(customer.Data).Select(x => x.Value)
};
Index(x => x.Query, FieldIndexing.Analyzed);
}
}
When this gets deployed, it looks like this:
from customer in docs.DataViewOfCustomer
where customer.Data != null
select new {
Query = customer.Data.Select(x => x.Value)
}
This doesn't work obviously, and if I change DataViewOfCustomer to "customers" it works just fine.
I'd rather not have to use non-type-checked (string) indexes to deploy. Is there a way to set the collection name that from the AbstractIndexCreationTask class?
Update
Since my data class is generic, I made a generic index which fixes up the names.
public class DataViewQuery<TEntity>
: AbstractIndexCreationTask<DataView<TEntity>, DataViewQueryResult>
{
private readonly string _entityName;
private readonly string _indexName;
// this is to fix the collection name for the index name
public override string IndexName { get { return _indexName; } }
// this is to fix the collection name for the index query
public override void Execute(IDatabaseCommands databaseCommands, DocumentConvention documentConvention)
{
var conventions = documentConvention.Clone();
conventions.FindTypeTagName =
type =>
typeof(DataView<TEntity>) == type
? _entityName
: documentConvention.FindTypeTagName(type);
base.Execute(databaseCommands, conventions);
}
public DataViewQuery(string entityName)
{
_entityName = entityName;
_indexName = String.Format("{0}/{1}", entityName, "Query");
Map = items =>
from item in items
where item.Data != null
select new
{
Query = AsDocument(item.Data).Select(x => x.Value)
};
Index(x => x.Query, FieldIndexing.Analyzed);
}
}
public class DataViewQueryResult
{
public string Query { get; set; }
}
Then I can create a specific index which has all the configuration in it.
// sets the collection type (DataView<Customer>) for the index
public class CustomerQuery : DataViewQuery<Customer>
{
// sets the collection name for the index
public CustomerQuery() : base(EntityName.Customers) { }
}
You need to configure this in the conventions.
The property to configure is FindTypeTagName

How to turn off cascade deleting?

I have class with back reference:
public class Employee : Entity
{
private string _Name;
private string _Position;
private Employee _SupervisorBackRef;
private IList<Employee> _Subordinates;
private IList<BusinessPartner> _BusinessPartners;
public virtual string Name
{
get { return _Name; }
set { _Name = value; }
}
public virtual string Position
{
get { return _Position; }
set { _Position = value; }
}
public virtual Employee SupervisorBackRef
{
get { return _SupervisorBackRef; }
set { _SupervisorBackRef = value; }
}
public virtual IList<Employee> Subordinates
{
get { return _Subordinates; }
set { _Subordinates = value; }
}
public virtual IList<BusinessPartner> BusinessPartners
{
get { return _BusinessPartners; }
set { _BusinessPartners = value; }
}
}
Because back reference SupervisorBackRef and Subordinates share same foreign key:
create table Employees (
Id INT not null,
Name NVARCHAR(255) null,
Position NVARCHAR(255) null,
EmployeeFk INT null,
primary key (Id)
)
Problem is that although I tryed override it, if I delete any supervisor, it delete all his Subordinates. I tryed this:
class EmployeeOverride : IAutoMappingOverride<Employee>
{
public void Override(FluentNHibernate.Automapping.AutoMapping<Employee> mapping)
{
mapping.HasMany(x => x.Subordinates).Cascade.None();
mapping.HasOne(x => x.SupervisorBackRef).Cascade.None();
}
}
But it isn't work. I tryed it changed to different another combinations, i tryed this override delete, but nothing help.
If I delete any Employee, it delete all his Subordinates.
I don't know how can i recognize if this override function working.
From many texts on internet i understand that Cascade.None() should work, most people has opposite problem, deleting/updating/... don't work, so I'm really confused.
Problem was, that I had in another place (in HasManyConvention.Apply) set in instance Cascade.AllDeleteOrphan() and it was higher priority.
PS: I don't know if is beter similar post like that answered od deleted. For me it looks like a problem which I had more time in past, but it is variation on "I dont't know my own code..."