I'm using Entity Framework CTP5.
I have a schema like this:
A group contains many textdescriptions.
A textdescriptions has many texts.
A Language has many texts.
So there are 4 tables.
Groups one-to-many DEscriptions many-to-many Texts many-to-one Languages.
So I have a many-to-many relationship where the relation also holds data.
Definitions of Text and TextDescription ( since we can query on the Id for Group and Languages I havent added them here )
public class Text
{
public int TextID { get; set; }
public int TextDescriptionID { get; set; }
public int LanguageID { get; set; }
public string OriginalText { get; set; }
public bool IsValid { get; set; }
public DateTime Added { get; set; }
public DateTime Updated { get; set; }
public Language Language { get; set; }
public TextDescription TextDescription { get; set; }
public static Text GetMissingText(string input)
{
Text text = new Text();
text.OriginalText = "Missing: " + input;
text.IsValid = true;
text.TextDescription = new TextDescription()
{
IsStatic = true,
Name = input,
IsMultiline = false,
};
return text;
}
}
public class TextDescription
{
public int TextDescriptionId { get; set; }
public int TextDescriptionGroupId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool UseHtml { get; set; }
public bool IsMultiline { get; set; }
public bool IsStatic { get; set; }
public TextDescriptionGroup TextDescriptionGroup { get; set; }
public virtual ICollection<Text> Texts { get; set; }
public static TextDescription GetNewItem(int textDescriptionGroupId)
{
var item = new TextDescription();
item.Name = item.Description = "n/a";
item.UseHtml = item.IsMultiline = item.IsMultiline = false;
item.TextDescriptionGroupId = textDescriptionGroupId;
return item;
}
}
When adding either a new language or a new text is inserted ... the many to many relation is not inserted into the database. (Think it would be a bad idea, so in the end, if thats the only solution, I could be able to that)
So how do I handle this in a smart way when I need to fetch all the text for a specific group from the database, but also get the translation if there are one for that languages.
I can't start fra the translation object, since its possible its not there. If I start to query from the Text entity ... how do I only select one language without getting all languages first.
repo.Find(x =>
x.GroupId == groupId &&
x.Translation.Any(a => a.LanguageID == id.Value)
);
I'm lost here ... any there any smart way ... so I wont have to query the database for all the Texts ... and then a query for each item ... to see if there are a translation? or else just make a new empty one.
In SQL I would do it like this:
SELECT TD.Name, T.OriginalText FROM TextDescriptions TD
LEFT JOIN Texts T ON TD.TextDescriptionId = T.TextDescriptionId
WHERE TextDescriptionGroupId = 41 AND ISNULL(T.LanguageId, 1) = 1
The above SQL will give me the elements even if there is not record now, I get a NULL for these values. I could then handle that it my code and avoid lazy load.
But can I get the same behavior in Entity Framework. I could see there would be some problems maybe for EF4 to do the mapping ... since I'm going from TextDesciptions to Texts ... and TextDesciptions have a List of Texts ... but here ... I only want either 1 or NULL, or just a new Entity that havent been added to the database yet.
Looking forward to some interesting answers.
mvh
For now ... if no other solution is found I will be running the follow SQL script to insert empty records. This way I'm sure the record is there when a user wants to edit it and dont have to ensure its there before saving it. Maybe also avoiding some naste Linq query.
I only have to run this SQL 2 places. When adding a new Language or new a new TextDesciption.
INSERT INTO Texts
SELECT TD.TextDescriptionId, L.LanguageId, '', 0, GETDATE(), GETDATE(), L.TwoLetterISOLanguageName
FROM TextDescriptions TD
INNER JOIN Languages L ON 1 = 1
LEFT JOIN Texts T ON
T.TextDescriptionId = TD.TextDescriptionId AND
T.LanguageId = L.LanguageId
WHERE TextId IS NULL
Related
I have List of Orders, which have the property "Status" which is an int. For each status I have a translations in different languages. I want to sort my list by the selected translation and not by numeric status value. What is the best practice here?
public record OrderTranslation
{
public string OrderStatus { get; set; }
public string StatusDescription { get; set; }
public Language Language { get; set; }
}
public record Order
{
public int? Id { get; set; }
public int Status { get; set; }
// I have added a new value to set the translated value and I want to order by this
public string TranslatedStatusValue { get; set;}
}
my function:
public async Task<FilterResult> FilterAsync(FilterRequest filterRequest, List<string> filterProperties, Language selectedLanguage)
{
var orderTranslations = dataContext
.OrderTranslations
.Where(ot => ot.Language == selectedLanguage)
.ToList();
var orders = dataContext.Orders.AsNoTracking();
foreach (var order in orders)
{
var description = orderTranslations
.Single(x => x.OrderStatus == serviceContract.Status)
.StatusDescription;
serviceContract.TranslatedValue = description;
}
// The TranslatedValue is always empty here
// This is not working, but I want to Order by the translation. Is there another possibility to to this, not using an extra property?
IQueryable<ServiceContractOrder> query = orders
.OrderBy("TranslatedStatusValue", filterRequest.IsSortAscending)
.WhereMatchesFilter(filterRequest, filterProperties);
result.FilterHits = await query
.Skip(filterRequest.ItemsToSkip())
.Take(filterRequest.ItemsPerPage)
.Cast<object>()
.ToListAsync();
result.TotalCount = await query.CountAsync();
result.ObjectType = typeof(Order).AssemblyQualifiedName;
result.FilteredProperties = filterProperties;
}
It all depends on your size of data and what you want to achieve.
If you have small data set, without pagination, you can sort them in client side ( in your dotnet code).
If you have a large dataset, and/or you need pagination, then you will need to apply the sorting to the DB. In such case, I would suggest you to store the translated values in Same table as Owned Entity or maybe different table. And then you can apply sorting in your LINQ query.
Two benefits you get is,
Sorting is absolute, and order is maintained across queries.
Performance, as sorting on client-side hurts for large data sets.
What you lose,
Any change to translation has to be applied to DB. This makes your database complex.
If the order status is a fixed set of data, like enum, then you can chose to have a denormalized design. i.e., to have a dedicated OrderStatus table with translations and then join them to your Order table.
Your domain will be somewhat like,
public record OrderStatus
{
int Id{get; set;}
public ISet<OrderTranslation> Translations { get; set; }
}
public record OrderTranslation(Language Language, string)
{
public string OrderStatus { get; set; }
public string StatusDescription { get; set; }
public Language Language { get; set; }
}
public record Order
{
public int? Id { get; set; }
public OrderStatus Status { get; set; }
}
Consider an Sqlite database, whose partial schema is shown below (we are not considering the Book_Tag table here). Note the many-to-many relationship between media items and tags using the link table Media_Tag:
An object model for these tables is as follows:
public enum MediaType
{
Dvd,
BluRay,
Cd,
Vhs,
Vinyl,
Other
}
public class MediaItem
{
public MediaType type { get; set; }
public long number { get; set; }
public int runningTime { get; set; }
public int releaseYear { get; set; }
public ICollection<Tag> tags { get; set; }
}
public class Tag
{
public string name { get; set; }
}
currently, Dapper is being used to read from the Media table, but without considering tags. The code is as follows:
public IEnumerable<MediaItem> readAll()
{
using (var db = new SqliteConnection(this.connectionString))
{
db.Open();
var sql = "SELECT * FROM Media;";
return db.Query<MediaItem>(sql);
}
}
public MediaItem readById(int id)
{
using (var db = new SqliteConnection(this.connectionString))
{
db.Open();
var sql = "SELECT * FROM Media WHERE id = #id;";
var #params = new { id = id };
return db.Query<MediaItem>(sql, #params).First();
}
}
How to change this so that the tag property of MediaItem is considered when creating the objects, for both cases (read by id and read all rows from the table)? Is a join query required? I'm sure Dapper has a way of doing this nicely, but I don't know how it's done.
You are not interested in anything from the link table so something like this SQL should do:
SELECT M.Id, M.title, M.type, M.Number, M.image, M.runningTime, M.releaseYear, T.Id, T.Name FROM Media as M
INNER JOIN Media_Tag AS MT ON M.id = MT.mediaId
INNER JOIN Tags AS T ON T.id = MT.tagId
If SqLite allows you can use M.*, T.* instead.
I have taken the liberty to add Id properties to your entity classes. I think you are going to need it, otherwise all your tags will be different instead of being unique. You might make it work without it, but it should make your life easier.
public class MediaItem
{
public int Id { get; set; } // New
public MediaType type { get; set; }
public long number { get; set; }
public int runningTime { get; set; }
public int releaseYear { get; set; }
public ICollection<Tag> tags { get; set; }
}
public class Tag
{
public int Id { get; set; } // New
public string name { get; set; }
}
Since both your entity classes have a unique id, you will have to pick them up and make sure they are unique going through the results. We do that by using dictionaries to keep them. I'm only showing the ReadAll, you should be able to do ReadById accordingly.
string sql = "<see above>";
using (var db = new SqliteConnection(this.connectionString))
{
var mediaDictionary = new Dictionary<int, Media>();
var tagDictionary = new Dictionary<int, Tag>();
var list = db.Query<Media, Tag, Media>(
sql,
(media, tag) =>
{
Media mediaEntry;
if (!mediaDictionary.TryGetValue(media.Id, out mediaEntry))
{
// Haven't seen that one before, let's add it to the dictionary
mediaEntry = media;
mediaDictionary.Add(mediaEntry.Id, mediaEntry);
}
Tag tagEntry;
if (!tagDictionary.TryGetValue(tag.Id, out tagEntry))
{
// Haven't seen that one before, let's add it to the dictionary
tagEntry = tag;
tagDictionary.Add(tagEntry.Id, tagEntry);
}
// Add the tag to the collection
mediaEntry.Tags.Add(tagEntry);
return mediaEntry;
},
splitOn: "Id") // This default and could be omitted
.Distinct()
.ToList();
I have a model in Entity Framework Core that goes something like this:
public class Anime
{
public int EpisodeCount { get { return Episodes.Count() } }
public virtual ICollection<Episode> Episodes { get; set; }
}
I'm having the issue of EpisodeCount being 0. The solution currently is to run a .Include(x => x.Episodes) within my EF query, but that loads the entire collection of episodes where it's not needed. This also increases my HTTP request time, from 100ms to 700ms which is just not good.
I'm not willing to sacrifice time for simple details, so is there a solution where I can have EF only query the COUNT of the episodes, without loading the entire collection in?
I was suggested to do this
var animeList = context.Anime.ToPagedList(1, 20);
animeList.ForEach(x => x.EpisodeCount = x.Episodes.Count());
return Json(animeList);
but this also returns 0 in EpisodeCount, so it's not a feasible solution.
You need to project the desired data into a special class (a.k.a. ViewModel, DTO etc.). Unfortunately (or not?), in order to avoid N + 1 queries the projection must not only include the count, but all other fields as well.
For instance:
Model:
public class Anime
{
public int Id { get; set; }
public string Name { get; set; }
// other properties...
public virtual ICollection<Episode> Episodes { get; set; }
}
ViewModel / DTO:
public class AnimeInfo
{
public int Id { get; set; }
public string Name { get; set; }
// other properties...
public int EpisodeCount { get; set; }
}
Then the following code:
var animeList = db.Anime.Select(a => new AnimeInfo
{
Id = a.Id,
Name = a.Name,
EpisodeCount = a.Episodes.Count()
})
.ToList();
produces the following single SQL query:
SELECT [a].[Id], [a].[Name], (
SELECT COUNT(*)
FROM [Episode] AS [e]
WHERE [a].[Id] = [e].[AnimeId]
) AS [EpisodeCount]
FROM [Anime] AS [a]
I have a problem when I am updating data to database. When I want to update data, Entitiy Framework adds new rows to tables that can have multiple rows (tables that have foreign key).
Database model:
When I update Phone/Contact or Tags entity, Entity Framework automatically adds new row instead of updating it
Here is code that I used:
public string UpdateContact(Contact contact)
{
if (contact != null)
{
int id = Convert.ToInt32(contact.id);
Contact Updatecontact = db.Contacts.Where(a => a.id == id).FirstOrDefault();
Updatecontact.firstname = contact.firstname;
Updatecontact.lastname = contact.lastname;
Updatecontact.address = contact.address;
Updatecontact.bookmarked = contact.bookmarked;
Updatecontact.city = contact.city;
Updatecontact.notes = contact.notes;
Updatecontact.Emails1 = contact.Emails1;
Updatecontact.Phones1 = contact.Phones1;
Updatecontact.Tags1 = contact.Tags1;
db.SaveChanges();
return "Contact Updated";
}
else
{
return "Invalid Record";
}
}
EDIT:
Here is EF Model code:
Contact:
public partial class Contact
{
public Contact()
{
this.Emails1 = new HashSet<Email>();
this.Phones1 = new HashSet<Phone>();
this.Tags1 = new HashSet<Tag>();
}
public int id { get; set; }
public string firstname { get; set; }
public string lastname { get; set; }
public string address { get; set; }
public string city { get; set; }
public Nullable<byte> bookmarked { get; set; }
public string notes { get; set; }
public virtual ICollection<Email> Emails1 { get; set; }
public virtual ICollection<Phone> Phones1 { get; set; }
public virtual ICollection<Tag> Tags1 { get; set; }
}
Emails/Tags and Phone have same model (with different name for value)
public partial class Email
{
public int id { get; set; }
public int id_contact { get; set; }
public string email1 { get; set; }
public virtual Contact Contact1 { get; set; }
}
Update properties rather than set new objects.
Updatecontact.Emails1.email1 = contact.Emails1.email1;
Updatecontact.Phones1.number = contact.Phones1.number;
Updatecontact.Tags1.tag1 = contact.Tags1.tag1;
Edit: seems that your contact model has lists of emails, phones and tags. If this is so, then simple assignment won't work. Instead, when sent from the client, you have to find one-by-one and update:
foreach ( var email in contact.Emails1 )
{
// first make sure the object is retrieved from the database
var updateemail = Updatecontact.Emails1.FirstOrDefault( e => e.id == email.id );
// then update its properties
updateemail.email1 = email.email1;
}
// do the same for phones and tags
It's doing that because you're setting the different HashSet values to the values of a completely different collection, namely from what you call contact in that method. In order for you to properly do an update, you're going to have to loop through the emails, phones, and tags to check if those need to be added/updated/deleted on the actual object that you're trying to update.
First, why do you have to search for the contact if you are already receiving it by parameter? That makes me think that you are creating a new one because you are in a different context, if so, then it creates a new record because you have 2 different object in 2 different context.
Try using just one object in the same context to update, EF should mark the object to modification by itself, if not then try making sure before saving that your object has EntityState.Modified.
I am working on an application for Windows Phone platform. Run into a question.
I'd 2 different List, 1 is direct read from xml file, and the other by some calculation. And I want to merge this two lists into 1, so I can display it out.
List 1:
public class studentClass1
{
public string studentID { get; set; }
public string studentFirstName { get; set; }
public string studentLastName { get; set; }
}
List 2:
public class studentClass2
{
public string studentID { get; set; }
public string studentGradePoint { get; set; }
}
First of all, I had readout the studentClass1 via
var studentList= from query in studentIndex.Descendants("student")
select new driversClass
StudentList1 = studentList.ToList();
Secondly, I process the student Grade Point calculation on the function and output to the 2nd list :
studentClass2 SG = new studentClass2
{
studentID = thestudentID ,
studentGradePoint = thestudentGradePoint .ToString()
};
StudentList2.Add(SG);
studentListAll = StudentList1 + StudentList2
now, I want to join this two list together so that I can output to screen by calling
studentResultListBox.Itemsource = StudentListAll;
any suggestion the code how would look like?
Thanks.
Assuming you just want to combine the appropriate info from both lists (it is not totally clear from your question) - introduce a third class studentClass3 that holds all the properties you want and use a join to match instances with a matching studentID:
var studentList3 = (from s1 in studentList1
join s2 in studentList2 on s1.studentID equals s2.studentID
select new studentClass3()
{
studentFirstName = s1.studentFirstName,
studentID = s1.studentID,
studentGradePoint = s2.studentGradePoint,
studentLastName = s1.studentLastName
}).ToList();
In general this problem should be rather solved when you read in the XML than trying to combine the lists later on - having three different classes for students might be confusing. Also take a look at the recommended naming conventions, they are a little off.
Use interface, e.g.
interface IStudent
{
string studentID { get; }
// other common properties below
}
public class StudentClass1 : IStudent
{
public string studentID { get; set; }
public string studentFirstName { get; set; }
public string studentLastName { get; set; }
}
public class StudentClass2 : IStudent
{
public string studentID { get; set; }
public string studentGradePoint { get; set; }
}
and then
studentResultListBox.Itemsource = list1.Cast<IStudent>()
.Concat(list2.Cast<IStudent>())
.ToList();
Or, if the inheritance is not the case, just cast everything to System.Object (+ override object.ToString)
studentResultListBox.Itemsource = list1.Cast<object>()
.Concat(list2.Cast<object>())
.ToList();