I'm having a Entity-Set Countries, reflecting a database table '<'char(2),char(3),nvarchar(50> in my database.
Im having a parser that returns a Country[] array of parsed countries, and is having issues with getting it updated in the right way. What i want is: Take the array of countries, for those countries not already in the database insert them, and those existing update if any fields is different. How can this be done?
void Method(object sender, DocumentLoadedEvent e)
{
var data = e.ParsedData as Country[];
using(var db = new DataContractEntities)
{
//Code missing
}
}
I was thinking something like
for(var c in data.Except(db.Countries)) but it wount work as it compares on wronge fields.
Hope anyone have had this issues before, and have a solution for me. If i cant use the Country object and insert/update an array of them easy, i dont see much benefict of using the framework, as from performers i think its faster to write a custom sql script that inserts them instead of ect checking if an country is already in the database before inserting?
Solution
See answer of post instead.
I added override equals to my country class:
public partial class Country
{
public override bool Equals(object obj)
{
if (obj is Country)
{
var country = obj as Country;
return this.CountryTreeLetter.Equals(country.CountryTreeLetter);
}
return false;
}
public override int GetHashCode()
{
int hash = 13;
hash = hash * 7 + (int)CountryTreeLetter[0];
hash = hash * 7 + (int)CountryTreeLetter[1];
hash = hash * 7 + (int)CountryTreeLetter[2];
return hash;
}
}
and then did:
var data = e.ParsedData as Country[];
using (var db = new entities())
{
foreach (var item in data.Except(db.Countries))
{
db.AddToCountries(item);
}
db.SaveChanges();
}
I would do it straightforward:
void Method(object sender, DocumentLoadedEvent e)
{
var data = e.ParsedData as Country[];
using(var db = new DataContractEntities)
{
foreach(var country in data)
{
var countryInDb = db.Countries
.Where(c => c.Name == country.Name) // or whatever your key is
.SingleOrDefault();
if (countryInDb != null)
db.Countries.ApplyCurrentValues(country);
else
db.Countries.AddObject(country);
}
db.SaveChanges();
}
}
I don't know how often your application must run this or how many countries your world has. But I have the feeling that this is nothing where you must think about sophisticated performance optimizations.
Edit
Alternative approach which would issue only one query:
void Method(object sender, DocumentLoadedEvent e)
{
var data = e.ParsedData as Country[];
using(var db = new DataContractEntities)
{
var names = data.Select(c => c.Name);
var countriesInDb = db.Countries
.Where(c => names.Contains(c.Name))
.ToList(); // single DB query
foreach(var country in data)
{
var countryInDb = countriesInDb
.SingleOrDefault(c => c.Name == country.Name); // runs in memory
if (countryInDb != null)
db.Countries.ApplyCurrentValues(country);
else
db.Countries.AddObject(country);
}
db.SaveChanges();
}
}
The modern form, using later EF versions would be:
context.Entry(record).State = (AlreadyExists ? EntityState.Modified : EntityState.Added);
context.SaveChanges();
AlreadyExists can come from checking the key or by querying the database to see whether the item already exists there.
You can implement your own IEqualityComparer<Country> and pass that to the Except() method. Assuming your Country object has Id and Name properties, one example of that implementation could look like this:
public class CountryComparer : IEqualityComparer<Country>
{
public bool Equals(Country x, Country y)
{
return x.Name.Equals(y.Name) && (x.Id == y.Id);
}
public int GetHashCode(Country obj)
{
return string.Format("{0}{1}", obj.Id, obj.Name).GetHashCode();
}
}
and use it as
data.Countries.Except<Country>(db, new CountryComparer());
Although, in your case it looks like you just need to extract new objects, you can use var newCountries = data.Where(c => c.Id == Guid.Empty); if your Id is Guid.
The best way is to inspect the Country.EntityState property and take actions from there regarding on value (Detached, Modified, Added, etc.)
You need to provide more information on what your data collection contains i.e. are the Country objects retrieved from a database through the entityframework, in which case their context can be tracked, or are you generating them using some other way.
I am not sure this will be the best solution but I think you have to get all countries from DB then check it with your parsed data
void Method(object sender, DocumentLoadedEvent e)
{
var data = e.ParsedData as Country[];
using(var db = new DataContractEntities)
{
List<Country> mycountries = db.Countries.ToList();
foreach(var PC in data)
{
if(mycountries.Any( C => C.Name==PC.Name ))
{
var country = mycountries.Any( C => C.Name==PC.Name );
//Update it here
}
else
{
var newcountry = Country.CreateCountry(PC.Name);//you must provide all required parameters
newcountry.Name = PC.Name;
db.AddToCountries(newcountry)
}
}
db.SaveChanges();
}
}
Related
Seems that the SaveChanges() only saves the last object added into the table:
static void Main(string[] args)
{
Category c = new Category();
using (GenericDBEntities db = new GenericDBEntities())
{
foreach (Match i in db.Matches)
{
if (!db.Categories.Any())
{
c.CategoryInternalId = i.CategoryId;
c.CategoryName = i.CategoryName;
c.SportId = i.SportId;
db.Categories.Add(c);
}
else
{
foreach (Category a in db.Categories)
{
if (i.CategoryId != a.CategoryInternalId)
{
c.CategoryInternalId = i.CategoryId;
c.CategoryName = i.CategoryName;
c.SportId = i.SportId;
db.Categories.Add(c);
}
else
{
return;
}
}
}
}
db.SaveChanges();
I have tried it in a few different ways which all have the same or less result, the for loop saves them all, no matter if condition is met or not. Why is it only saving the last object of the Match table? What am I missing?
Clarification: The task is to go through Match table, which has duplicate CategoryIDs and to, if it is not already in the Category table, add it, so it won't store duplicates again, the problem is Category table is initially empty, hence the newbie logic, still learnin'!
You need to create the Category object for each inserted category, otherwise you have just one category object which you are editing all the time and hence just this one (the last edit) will be saved to the DB.
If you want to add all new categories from Matches based on CategoryId you can use something like this:
using (GenericDBEntities db = new GenericDBEntities())
{
var newCategories = db.Matches
.Where(m => !db.Categories
.Select(c => c.CategoryInternalId)
.Distinct().Contains(m.CategoryId))
.Select(m => new { m.CategoryId, m.CategoryName, m.SportId })
.GroupBy(m => m.otherid)
.Select(g => g.FirstOrDefault())
.ToList();
foreach (var i in newCategories)
{
var c = new Category()
{
CategoryInternalId = i.CategoryId,
CategoryName = i.CategoryName,
SportId = i.SportId
};
db.Categories.Add(c);
}
db.SaveChanges();
}
Now you'll get all the new categories from the Matches table in one go. This is a first draft, meaning that the performance can be further tuned if needed.
I want to prevent documents from being deleted in my project and I decided to use metadata to mark document as Archived. I used below code to do that:
public class DeleteDocumentListener : IDocumentDeleteListener
{
public void BeforeDelete(string key, object entityInstance, RavenJObject metadata)
{
metadata.Add("Archived", true);
throw new NotSupportedException();
}
}
After that I wanted to alter query to return only documents which have Archived metadata value set to false:
using (var session = _store.OpenSession())
{
var query = session.Advanced.DocumentQuery<Cutter>()
.WhereEquals("#metadata.Archived", false);
}
Unfortunately this query return empty result set. It occurs that if Document doesn't have this metadata property then above condition is treated as false. It wasn't what I expected.
How can I compose query to return Documents which don't have metadata property or this property has some value ?
You can solve it by creating an index for you Cutter documents and then query against that:
public class ArchivedIndex : AbstractIndexCreationTask<Cutter>
{
public class QueryModel
{
public bool Archived { get; set; }
}
public ArchivedIndex()
{
Map = documents => from doc in documents
select new QueryModel
{
Archived = MetadataFor(doc)["Archived"] != null && MetadataFor(doc).Value<bool>("Archived")
};
}
}
Then query it like this:
using (var session = documentStore.OpenSession())
{
var cutters = session.Query<ArchivedIndex.QueryModel, ArchivedIndex>()
.Where(x => x.Archived == false)
.OfType<Cutter>()
.ToList();
}
Hope this helps!
Quick side note. To create the index, the following code may need to be run:
new ArchivedIndex().Execute(session.Advanced.DocumentStore);
Can someone please have a look at my code, I think there must be a way to optimize the foreach piece of code?
I have a database with Artists, each artist has multiple songTitles (called Titles), and each Title can have multiple Meanings.
Artist [1..*] Title [1..*] Meaning [0..*]
I want to find the count of Meanings, per Title, for an Artist, and return it as a new ViewModel List.
public class TitleVM
{
public int TitleID { get; set; }
public int MeaningCount { get; set; }
}
public List<TitleVM> GetTitlesByArtistID(int artistID)
{
//find the artist by ID
var titles = context.Titles.Where(x => x.ArtistID == artistID);
//create new VMList to be returned
var titleVMList = new List<TitleVM>();
//loop through each title,
foreach (var item in titles)
{
//find the number of meanings,
var count = 0;
if (item.Meanings != null && item.Meanings.Count > 0)
{
count = item.Meanings.Count();
}
// and map it to VM, add to list
titleVMList.Add(new TitleVM
{
TitleID = TitleID,
MeaningCount = count
});
}
return titleVMList;
}
I thought mapping it would be easiest, but have no idea how to map a viewmodel with lists in this way.
In my project I use Omu.ValueInjecter for mapping basic models, because Automapper needs full trust to run, and my host doesn't allow it.
Let me know if more information is needed.
Ok I read that its better to do an .AddRange then adding the the item with .Add each time.
I got my code down to the below:
public int CountMeanings(IEnumerable<Meaning> meanings)
{
if (meanings != null && meanings.Count() > 0)
return meanings.Count();
return 0;
}
public List<TitleVM> GetTitlesByArtistID(int artistID)
{
var titleVMList = new List<TitleVM>();
var titles = context.Titles.Where(x => x.ArtistID == artistID).AsEnumerable();
titleVMList.AddRange(titles.Select(item => new TitleVM {
TitleID = item.TitleID,
MeaningCount = CountMeanings(item.Meanings)
}));
return titleVMList;
}
I modified the "Read" operation on my Windows Azure Mobile Services Preview table (named "Item") as follows:
Javascript:
function read(query, user, request)
{
var howRead;
if(howRead == "unique")
{
var sqlUnique = "SELECT DISTINCT ? FROM Item WHERE qProjectCode = ?";
mssql.query(sqlUnique)
request.execute();
}
else if (howRead == "column")
{
var sqlColumn = "SELECT ? FROM Item WHERE qProjectCode = ?";
mssql.query(sqlColumn)
request.execute();
}
else if (howRead == "all")
{
var sqlAll = "SELECT * FROM Item WHERE qProjectCode = ?";
mssql.query(sqlAll)
request.execute();
}
}
This simply species when I want a unique list of a single column's values returned, all items in a single column, or all columns, respectively, all while limiting the read to those records with a given project code.
Right now, this works in C#, but scans the entire table (with other project codes) and always returns all columns. This is inherently inefficient.
c#
var client = new MobileServiceClient("[https path", "[key]");
var table = client.GetTable<Item>();
var query1 = table.Where(w => w.QProjectCode == qgv.projCode && w.QRecord == (int)lbRecord.Items[uStartRecordIndex]);
var query1Enum = await query1.ToEnumerableAsync();
foreach (var i in query1Enum)
{
// process data
}
How do I alter the c# code to deal with the Javascript code? Feel free to critique the overall approach, since I am not a great programmer and can always use advice!
Thanks
A few things:
In your server code, the mssql calls are not doing anything (useful). If you want to get their results, you need to pass a callback (the call is asynchronous) to it.
Most of your scenarios can be accomplished at the client side. The only for which you'll need server code is the one with the DISTINCT modifier.
For that scenario, you'll need to pass a custom parameter to the server script. You can use the WithParameters method in the MobileServiceTableQuery<T> object to define parameters to pass to the service.
Assuming this data class:
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Other { get; set; }
public string ProjectCode { get; set; }
}
The code below can be used to accomplish the scenarios 2 and 3 at the client side only (no script needed at the server side). The other one will need some script, which I'll cover later.
Task<IEnumerable<string>> ReadingByColumn(IMobileServiceTable<Item> table, string projectCode)
{
return table
.Where(i => i.ProjectCode == projectCode)
.Select(i => i.Name)
.ToEnumerableAsync();
}
Task<IEnumerable<Item>> ReadingAll(IMobileServiceTable<Item> table, string projectCode)
{
return table.Where(i => i.ProjectCode == projectCode).ToEnumerableAsync();
}
Task<IEnumerable<string>> ReadingByColumnUnique(IMobileServiceTable<Item> table, string projectCode)
{
var dict = new Dictionary<string, string>
{
{ "howRead", "unique" },
{ "projectCode", projectCode },
{ "column", "Name" },
};
return table
.Select(i => i.Name)
.WithParameters(dict)
.ToEnumerableAsync();
}
Now, to support the last method (which takes the parameters, we'll need to do this on the server script:
function read(query, user, request)
{
var howRead = request.parameters.howRead;
if (howRead) {
if (howRead === 'unique') {
var column = request.parameters.column; // WARNING: CHECK FOR SQL INJECTION HERE!!! DO NOT USE THIS IN PRODUCTION!!!
var sqlUnique = 'SELECT DISTINCT ' + column + ' FROM Item WHERE ProjectCode = ?';
mssql.query(sqlUnique, [request.parameters.projectCode], {
success: function(distinctColumns) {
var results = distinctColumns.map(function(item) {
var result = [];
result[column] = item; // mapping to the object shape
return result;
});
request.respond(statusCodes.OK, results);
}
});
} else {
request.respond(statusCodes.BAD_REQUEST, {error: 'Script does not support option ' + howRead});
}
} else {
// no server-side action needed
request.execute();
}
}
Is there a way to specify a table to use for Hi-Lo values, with each entity having a per-row entry, via a convention (while still having nHibernate create the table structure for you)? I would like to replicate what Phil Haydon blogged about here, but without having to manually manage the table. As it stands, migrating his row-per-table code to its own convention will work only if you've already created the appropriate entries for 'TableKey' in the table already.
Alternatively, is this possible via the XML mappings?
And if all else fails, is the only other appropriate option to use a custom generator, a la this post?
Fabio Maulo talked about this in one of his mapping-by-code posts.
Mapping by code example:
mapper.BeforeMapClass += (mi, type, map) =>
map.Id(idmap => idmap.Generator(Generators.HighLow,
gmap => gmap.Params(new
{
table = "NextHighValues",
column = "NextHigh",
max_lo = 100,
where = string.Format(
"EntityName = '{0}'", type.Name.ToLowerInvariant())
})));
For FluentNHibernate, you could do something like:
public class PrimaryKeyConvention : IIdConvention
{
public void Apply(IIdentityInstance instance)
{
var type = instance.EntityType.Name;
instance.Column(type + "Id");
instance.GeneratedBy.HiLo(type, "NextHigh", "100",
x => x.AddParam("where", String.Format("EntityName = '{0}'", type));
}
}
Also, Fabio explained how you could use IAuxiliaryDatabaseObject to create Hi-Lo script.
private static IAuxiliaryDatabaseObject CreateHighLowScript(
IModelInspector inspector, IEnumerable<Type> entities)
{
var script = new StringBuilder(3072);
script.AppendLine("DELETE FROM NextHighValues;");
script.AppendLine(
"ALTER TABLE NextHighValues ADD EntityName VARCHAR(128) NOT NULL;");
script.AppendLine(
"CREATE NONCLUSTERED INDEX IdxNextHighValuesEntity ON NextHighValues "
+ "(EntityName ASC);");
script.AppendLine("GO");
foreach (var entity in entities.Where(x => inspector.IsRootEntity(x)))
{
script.AppendLine(string.Format(
"INSERT INTO [NextHighValues] (EntityName, NextHigh) VALUES ('{0}',1);",
entity.Name.ToLowerInvariant()));
}
return new SimpleAuxiliaryDatabaseObject(
script.ToString(), null, new HashedSet<string> {
typeof(MsSql2005Dialect).FullName, typeof(MsSql2008Dialect).FullName
});
}
You would use it like this:
configuration.AddAuxiliaryDatabaseObject(CreateHighLowScript(
modelInspector, Assembly.GetExecutingAssembly().GetExportedTypes()));
For users of Fluent NHibernate, Anthony Dewhirst has posted a nice solution over here: http://www.anthonydewhirst.blogspot.co.uk/2012/02/fluent-nhibernate-solution-to-enable.html
Building off of Anthony Dewhirst's already excellent solution, I ended up with the following, which adds a couple improvements:
Adds Acceptance Criteria so that it doesn't try to handle non-integral Id types (e.g. Guid) and won't stomp on Id mappings which have a generator explicitly set
Script generation takes Dialect into consideration
public class HiLoIdGeneratorConvention : IIdConvention, IIdConventionAcceptance
{
public const string EntityColumnName = "entity";
public const string MaxLo = "500";
public void Accept(IAcceptanceCriteria<IIdentityInspector> criteria)
{
criteria
.Expect(x => x.Type == typeof(int) || x.Type == typeof(uint) || x.Type == typeof(long) || x.Type == typeof(ulong)) // HiLo only works with integral types
.Expect(x => x.Generator.EntityType == null); // Specific generator has not been mapped
}
public void Apply(IIdentityInstance instance)
{
instance.GeneratedBy.HiLo(TableGenerator.DefaultTableName, TableGenerator.DefaultColumnName, MaxLo,
builder => builder.AddParam(TableGenerator.Where, string.Format("{0} = '{1}'", EntityColumnName, instance.EntityType.FullName)));
}
public static void CreateHighLowScript(NHibernate.Cfg.Configuration config)
{
var dialect = Activator.CreateInstance(Type.GetType(config.GetProperty(NHibernate.Cfg.Environment.Dialect))) as Dialect;
var script = new StringBuilder();
script.AppendFormat("DELETE FROM {0};", TableGenerator.DefaultTableName);
script.AppendLine();
script.AppendFormat("ALTER TABLE {0} {1} {2} {3} NOT NULL;", TableGenerator.DefaultTableName, dialect.AddColumnString, EntityColumnName, dialect.GetTypeName(SqlTypeFactory.GetAnsiString(128)));
script.AppendLine();
script.AppendFormat("CREATE NONCLUSTERED INDEX IX_{0}_{1} ON {0} ({1} ASC);", TableGenerator.DefaultTableName, EntityColumnName);
script.AppendLine();
if (dialect.SupportsSqlBatches)
{
script.AppendLine("GO");
script.AppendLine();
}
foreach (var entityName in config.ClassMappings.Select(m => m.EntityName).Distinct())
{
script.AppendFormat("INSERT INTO [{0}] ({1}, {2}) VALUES ('{3}',1);", TableGenerator.DefaultTableName, EntityColumnName, TableGenerator.DefaultColumnName, entityName);
script.AppendLine();
}
if (dialect.SupportsSqlBatches)
{
script.AppendLine("GO");
script.AppendLine();
}
config.AddAuxiliaryDatabaseObject(new SimpleAuxiliaryDatabaseObject(script.ToString(), null));
}
}