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.
Related
I am having some issues trying to figure out how to display the names of the state and city instead of the StateId and CityId.
I am aware that you can put an instance of States and Cities in the model like this:
public virtual Cities Cities { get; set; }
public virtual States States { get; set; }
And then add this to the view:
#Html.DisplayFor(modelItem => item.Cities.CityName)
#Html.DisplayFor(modelItem => item.States.StateName)
However I have a few things that do not allow me to do this without error.
I use a ViewModel that gives me what I need for the view. It relies on a list from Addresses Table and in the Addresses Model I have Select Lists for these so that I can populate a dropdown list for each on edit and create.
When I add these to this to the Model and remove the selectLists I get an error that says it cannot find the Column Cities_CityId, States_StateId.
When I add it to the viewModel it just isn't available on the view. If I only needed it while showing 1 record I can use
Model.Cities.CityName
But I am retrieving several records, if they exist and using that would display the same city and state for all records, I believe..
I believe that all of my joins are correct but could be missing something.
Below is the ViewModel:
public partial class AddressOverview
{
public static AddressOverview GetAddressByCompany(int id, GeneralEntities db)
{
var qCus = from ad in db.Addresses
join cn in db.CompanyNames on ad.CompanyId equals cn.CompanyId
join cit in db.Cities on ad.City equals cit.CityId
join st in db.States on ad.State equals st.StateId
where (ad.CompanyId == id)
select new AddressOverview()
{
AddressId = ad.AddressId,
Customer = cn.CompanyName,
Location = ad.LocationName,
Addresses = ad,
CompanyId = ad.CompanyId,
State = st.StateName,
City = cit.CityName
};
var result = qCus.FirstOrDefault();
if (result != null)
{
result.AddressDetail = db.Addresses.Where(a => a.CompanyId == result.CompanyId);
};
return result;
}
public int AddressId { get; set; }
public string Customer { get; set; }
public string Location { get; set; }
public int CompanyId { get; set; }
public string City { get; set; }
public string State { get; set; }
public virtual Addresses Addresses { get; set; }
public virtual CompanyNames CompanyName { get; set; }
public virtual Cities Cities { get; set; }
public virtual States States { get; set; }
public virtual IEnumerable<Addresses> AddressDetail { get; set; }
}
And here is the controller:
public ActionResult Index(int id)
{
//Trying to get a view model for customer from the received UserId.
AddressOverview modelInstance = AddressOverview.GetAddressByCompany(id, db);
if (modelInstance == null)
{
//return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
return RedirectToAction("/Add/", new { id });
}
return View(modelInstance);
}
I am wondering if maybe there is a way to do this in the controller using the modelInstance? Please forgive me, I am trying to clean up some things that someone else did and my skills are a little weak.
If you need to see something else let me know.
Thanks for your help!
Update:
I just had a smack in the head moment.. Looking at View from question posted below I noticed that I was calling it from the Addresses Model instead of the ViewModel. So I corrected that but it still does not work..
Changed from this:
#model MyProject.GenerDTO.Entities.Addresses
To:
#model MyProject.Models.AddressOverview
Update:
Here is the solution I came up with. Probably not the best, but certainly a quick fix..
I decided to create a view on the MSSQL side and bring all of this together and then used that in the view and the ViewModel. If you decide to do something like this. Be sure to make sure that it is [NotMapped] in the entities. We are only using this to view and it is not meant to be used for editing, creating, etc.
Here is the new View Model:
public partial class AddressOverview
{
public static AddressOverview GetAddressByCompany(int id, GeneralEntities db)
{
var qCus = from ad in db.AddressView
join cn in db.CompanyNames on ad.CompanyId equals cn.CompanyId
where (ad.CompanyId == id)
select new AddressOverview()
{
AddressId = ad.AddressId,
Customer = cn.CompanyName,
Location = ad.LocationName,
AddressView = ad,
CompanyId = ad.CompanyId
};
var result = qCus.FirstOrDefault();
if (result != null)
{
result.AddressDetail = db.AddressView.Where(a => a.CompanyId == result.CompanyId);
};
return result;
}
public int AddressId { get; set; }
public string Customer { get; set; }
public string Location { get; set; }
public int CompanyId { get; set; }
public virtual AddressView AddressView { get; set; }
public virtual CompanyNames CompanyName { get; set; }
public virtual IEnumerable<AddressView> AddressDetail { get; set; }
}
I believe that creating a view in SQL is clean and the query is done server side. It can be treated like a table in the project. I am open to alternatives.
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();
The entity framework isn't super new to me, however is confusing as I continue to expand my data models. I am attempting to create a class that has an array of another class. Class A or County.cs has a list of Class B or Product.cs
I cannot seem to create write these classes in a way that when you ask for context.counties you also get the array of products attached to it.
Class A or County.cs
public class County
{
[Key]
public int ID { get; set; }
public String Name { get; set; }
public List<Product> Products { get; set; } = new List<Product>();
[NotMapped]
public DateTime firstAppearance {
get {
var data = (from obj in Products orderby obj.Date descending select obj.Date).FirstOrDefault();
if (this.softwareIn)
{
return data;
}
else
{
var date = new DateTime(1,1,1);
return date;
}
}
set {
this.firstAppearance = value;
}
}
[NotMapped]
public bool softwareIn {
get {
return Products.Count() >= 1;
}
set {
this.softwareIn = value;
}
}
}
Class B or Product.cs
public class Product
{
[Key]
public int ID { get; set; }
public String Name { get; set; }
public DateTime Date { get; set; }
[NotMapped]
public DateTime DateUtc {
get {
return getUtcDate();
}
set {
this.DateUtc = value;
}
}
public DateTime getUtcDate() {
return this.Date.ToUniversalTime();
}
}
I just don't understand and haven't created enough of 1:M relations in the entity framework. Why cannot I do something like this and have it work all the time? The first time I run this I get the type of data I expect, the xx county has a product. However if I remove all this and just return the context.counties I get nothing in the products array.
[Route("Counties")]
public object GetCounties() {
var data = new County() {
Name = "xxx",
};
data.Products.Add(new Product() { Name="Cool Software", Date = DateTime.Now});
db.Counties.Add(data);
db.SaveChanges();
var da = db.Counties.ToList();
return db.Counties;
}
The reason you have having this issue is because the foreign keys are not correctly configured. Take a look at your database and look at the foreign keys. For Entity Framework to understand the relationships properly, you must mark related entities as virtual. So do this:
public virtual List<Product> Products { get; set;}
And then in your Product class add the navigation property back to the parent County:
public virtual County County { get; set;}
I found this tutorial really good:
http://www.entityframeworktutorial.net/code-first/configure-one-to-many-relationship-in-code-first.aspx
Hope this helps.
I have an entity which holds a list of entities (same as root entity) to represent a Folder structure:
public class SopFolder
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime? LastUpdated { get; set; }
public int Status { get; set; }
public virtual ICollection<SopField> SopFields { get; set; }
public virtual ICollection<SopFolder> SopFolderChildrens { get; set; }
public virtual ICollection<SopBlock> Blocks { get; set; }
public virtual ICollection<SopReview> Reviews { get; set; }
}
This entity is stored in my DB using Code-First Approach which is working fine. I then print the entity to a KendoUI Treeview, let the user modify it and on "save" post it back to the Server to an Action as IEnumerable<TreeViewItemModel> items.
I then look for the ROOT entity with all it's children (there is only one root) and convert it back into an SopFolder object.
To get the full object updated in the database I do the following:
List<SopFolder> sopfolderlist = ConvertTree(items.First());
SopFolder sopfolder = sopfolderlist[0];
if (ModelState.IsValid)
{
SopFolder startFolder = new SopFolder { Id = sopfolder.Id };
//db.SopFolders.Attach(startFolder);
// db.SopFolders.Attach(sopfolder);
startFolder.Name = sopfolder.Name;
startFolder.LastUpdated = sopfolder.LastUpdated;
startFolder.SopFields = sopfolder.SopFields;
startFolder.SopFolderChildrens = sopfolder.SopFolderChildrens;
startFolder.Status = sopfolder.Status;
db.Entry(startFolder).State = EntityState.Modified;
db.SaveChanges();
return Content("true");
}
However this is not working. The model is not updated at all. If I shift the "entityState.Modified" before the modifications, it just creates a complete fresh duplicate of my data in the database (modified of course).
Is my approach correct or do I have to go a different path? What am I missing here? I guess there is another "hidden" id which lets the EF map the entities to the db entries but I am not sure about this. Thanks for help!
UPDATE:
Instead of creatinga new instance of SopFolder I also tried db.SopFolders.Find(sopfolder.Id) and this works for entries with no children. If I have entities with children, it creates a duplicate.
Regards,
Marcus
This is typical Disconnected Graph scenario. Please see this question for possible solutions:
Disconnected Behavior of Entity Framework when Updating Object Graph
You have already figure out the first solution - that is: update entities separately. Actually, what you should do is to fetch the original data from database and then do comparison of what have changed. There are some generic ways of doing that, some of them are described in "Programming EF DbContext" book by J.Lerman, which I strongly recommend to you before doing more coding using EF.
P.S. IMHO this is the worse downside of EF.
Replace SopFolder startFolder = new SopFolder { Id = sopfolder.Id }; with
SopFolder startFolder = db.SopFolders.FirstOrDefault(s=>s.Id.Equals(sopfolder.Id));
// then validate if startFolder != null
I recommend you to create your entity model with ParentId, not children object list. When you need treeview model collect it with recursive function from database.
public class SopFolder
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime? LastUpdated { get; set; }
public int Status { get; set; }
public virtual ICollection<SopField> SopFields { get; set; }
//public virtual ICollection<SopFolder> SopFolderChildrens { get; set; }
public int? ParentFolderId { get; set; }
public virtual ICollection<SopBlock> Blocks { get; set; }
public virtual ICollection<SopReview> Reviews { get; set; }
}
When you create children folders, select it's parent, so collect your data. In childrens case try this :
List<SopFolder> sopfolderlist = ConvertTree(items.First());
SopFolder sopfolder = sopfolderlist[0];
if (ModelState.IsValid)
{
SopFolder startFolder = new SopFolder { Id = sopfolder.Id };
//db.SopFolders.Attach(startFolder);
// db.SopFolders.Attach(sopfolder);
startFolder.Name = sopfolder.Name;
startFolder.LastUpdated = sopfolder.LastUpdated;
startFolder.SopFields = sopfolder.SopFields;
startFolder.SopFolderChildrens = sopfolder.SopFolderChildrens;
foreach (var child in sopfolder.SopFolderChildrens)
{
db.SopFolders.CurrentValues.SetValues(child);
db.SaveChanges();
}
startFolder.Status = sopfolder.Status;
db.Entry(startFolder).State = EntityState.Modified;
db.SaveChanges();
return Content("true");
}
I am new to LINQ and and come up with the below to add new information to my DB using LINQ and EF5 but I am sure there is a more efficant, better, way to do this I just don't know it. I was hoping to get some input on what I can do to acceive the same but with less/more efficant code.
using (var db = new FullContext())
{
if (ddlItemType.SelectedValue == "Other")
{
var NewItemType = new ItemType { Name = tbNewType.Text };
db.ItemTypes.Add(NewItemType);
db.SaveChanges();
}
if (ddlRegion.SelectedValue == "Other")
{
var NewRegion = new ReleaseRegion { Name = tbNewRegion.Text };
db.Regions.Add(NewRegion);
db.SaveChanges();
}
var NewItemTypeID = byte.Parse((from i in db.ItemTypes
where i.Name == tbNewType.Text
select new { i.ID }).ToString());
var NewRegionID = byte.Parse((from r in db.Regions
where r.Name == tbNewRegion.Text
select new { r.ID }).ToString());
var NewItem = new Item
{
Name = tbItemName.Text,
TypeID = NewItemTypeID,
RegionID = NewRegionID,
Condition = ddlCondition.SelectedValue.ToString(),
UPC = tbUPC.Text,
ISBN = tbISBN.Text,
IsColleciton = cbIsCollection.Checked,
CollectionID = Convert.ToInt16(ddlCollection.SelectedValue),
Notes = tbNotes.Text
};
db.Items.Add(NewItem);
db.SaveChanges();
}
Item.cs:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace FFCollection.DAL
{
[Table("Items")]
public class Item
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Int16 ID { get; set; }
[Required]
public string Name { get; set; }
public byte TypeID { get; set; }
[ForeignKey("TypeID")]
public virtual ItemType Type { get; set; }
public byte RegionID { get; set; }
[ForeignKey("RegionID")]
public virtual ReleaseRegion Region { get; set; }
[Required]
public string Condition { get; set; }
public string UPC { get; set; }
public string ISBN { get; set; }
public string Notes { get; set; }
[Required]
public Boolean IsColleciton { get; set; }
public Int16 CollectionID { get; set; }
[ForeignKey("CollectionID")]
public virtual Item InCollectionID { get; set; }
}
}
ItemType.cs:
using System.ComponentModel.DataAnnotations.Schema;
namespace FFCollection.DAL
{
public class ItemType
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public byte ID { get; set; }
public string Name { get; set; }
}
}
The databinding to DDL:
using (var db = new FullContext())
{
ddlItemType.DataSource = (from t in db.ItemTypes
select new { t.ID, t.Name }).ToList();
ddlItemType.DataTextField = "Name";
ddlItemType.DataValueField = "ID";
ddlItemType.DataBind();
ddlItemType.Items.Insert(0, new ListItem("Other", "Other"));
}
Part of the trouble isn't Linq, it's how you're using EF. Based on that example code you're using it as a data layer wrapper rather than an ORM. When constructing an object graph you should deal with the objects where you can, not foreign key IDs. The power of an ORM is that you can deal specifically with object graphs that are mapped to data, so that when you tell the ORM to save an object (and it's associated relatives) the ORM takes out all of the work of inserting/updating new records and wiring up keys. You're doing all that extra work in code, where an ORM like EF should allow you to accomplish what you want with a handful of lines.
For a start, when dealing with combo boxes, bind them to a data structure that includes the lookup value's ID that you can resolve instances of existing ItemTypes or Regions to associate with your new Item. (or in the case of selections of "other".
What I'd be looking at would be to bind the combo boxes to ItemType/Regions with the "Other" being a specific place-holder that the code will substitute with a new object if selected based on entries in the text fields. Then rather than saving the new objects before appending to the "Item", you simply set the references and save the Item which should cascade insert operations for the new lookup objects.
After this code executes EF will automatically put an ID into your NewItemType entity. You don't need to go and find it again, you could just say NewItemType.ID. This will only work after you have already called db.SaveChanges().
if (ddlItemType.SelectedValue == "Other")
{
var NewItemType = new ItemType { Name = tbNewType.Text };
db.ItemTypes.Add(NewItemType);
db.SaveChanges();
}