Updating database record using Entity Framework (lazy loading and virtual properties)
Asked Answered
M

1

1

I'm struggling with updating existing lecturer's data in database.
Every lecturer has Name, AcademicDegree and Courses which are taught by him/her (Courses==Lessons).

In fact there are more properties in class Lecturer but they are not relevant. For simplicity lets assume that POCO class is defined this way:

// POCO class (Entity Framework Reverse Engineering Code First)
public class Lecturer
{
    public Lecturer()
    {
        this.Courses = new List<Course>();
    }

    public int Id_Lecturer { get; set; } // Primary Key
    public string Name { get; set; }
    public int? Academic_Degree_Id { get; set; }
    public virtual AcademicDegree AcademicDegree { get; set; }
    public virtual ICollection<Course> Courses { get; set; }
}

In Data Access Layer I have method void UpdateLecturer(Lecturer lecturer) which should update lecturer whose Id_Lecturer is equal to lecturer.Id_Lecturer (using lecturer.Name, lecturer.AcademicDegree and lecturer.Courses).

It's very handy method because in ViewModel I can call _dataAccess.UpdateLecturer(SelectedLecturer) (where SelectedLecturer is binded in XAML; SelectedLecturer properties can be set by user in TextBoxes and Checkbox).

Unfortunatelly this method:

    public void UpdateLecturer(Lecturer lecturer)
    {
        using(var db = new AcademicTimetableDbContext())
        {
            // Find lecturer with Primary Key set to 'lecturer.Id_Lecturer':
            var lect = db.Lecturers.Find(lecturer.Id_Lecturer);

            // If not found:
            if (lect == null)
                return;

            // Copy all possible properties:
            db.Entry(lect).CurrentValues.SetValues(lecturer);
            // Everything was copied except 'Courses'. Why?!

            // I tried to add this, but it doesn't help:
            // var stateMgr = (db as IObjectContextAdapter).ObjectContext.ObjectStateManager;
            // var stateEntry = stateMgr.GetObjectStateEntry(lect);
            // stateEntry.SetModified();

            db.SaveChanges();
        }
    }

updates everything (i.e. Id_Lecturer, Name, Academic_Degree_Id and AcademicDegree) except Courses which are unchanged after db.SaveChanges().

Why? How can I fix it?


Similar problems:


-- Edit --

I also tried this way (idea came from this post):

public void UpdateLecturer(Lecturer lecturer)
{
    using (var db = new AcademicTimetableDbContext())
    {
        if (lecturer == null)
            return;

        DbEntityEntry<Lecturer> entry = db.Entry(lecturer);

        if (entry.State == EntityState.Detached)
        {
            Lecturer attachedEntity = db.Set<Lecturer>().Find(lecturer.Id_Lecturer);

            if (attachedEntity == null)
                entry.State = EntityState.Modified;
            else
                db.Entry(attachedEntity).CurrentValues.SetValues(lecturer);
        }

        db.SaveChanges();
    }
}

but still Courses don't overwrite old values.


-- Edit 2 --

In response to the @Slauma question I'll describe how I loaded SelectedLecturer (which is passed to UpdateLecturer(Lecturer lecturer) method as an argument).

I'm implementing MVVM concept so I have View project within my Solution with DataContext set to LecturerListViewModel. In View there is DataGrid with list of all lecturers fetched from databse. DataGrid is binded this way:

<DataGrid AutoGenerateColumns="False" Name="LecturersDataGrid" HeadersVisibility="Column" IsReadOnly="True" ItemsSource="{Binding Lecturers,Mode=TwoWay}" SelectedItem="{Binding SelectedLecturer, Mode=TwoWay}">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Name" Binding="{Binding Name}" />
        <DataGridTextColumn Header="Academic degree" Binding="{Binding AcademicDegree.Degree_Name}" />
        <DataGridTemplateColumn>
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Button Content="Edit" Click="EditButtonClick"/>
                        <Button Content="Delete" Command="{Binding DataContext.RemoveLecturer, ElementName=LecturersDataGrid}" />
                    </StackPanel>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

Lecturers are fetched from database in LecturerListViewModel constructor in this way:

///
/// Code within LecturerListViewModel class:
///

// All lecturers from database.
public ObservableCollection<Lecturer> Lecturers

// Constructor
public LecturerListViewModel()
{
    // Call to Data Access Layer:
    Lecturers = new ObservableCollection<Lecturer>(_dataAccess.GetAllLecturers());

    // Some other stuff here...
}

private Lecturer _selectedLecturer;

// Currently selected row with lecturer.
public Lecturer SelectedLecturer
{
    get { return _selectedLecturer; }
    set { SetProperty(out _selectedLecturer, value, x => x.SelectedLecturer); }
}

///
/// Data Access Layer (within DataAccess class):
/// 

public IEnumerable<Lecturer> GetAllLecturers()
{
    using (var dbb = new AcademicTimetableDbContext())
    {
        var query = from b 
                    in dbb.Lecturers.Include(l => l.AcademicDegree).Include(l => l.Timetables).Include(l => l.Courses)
                    select b;
        return query.ToList();
    }
}
Matthewmatthews answered 13/11, 2012 at 16:31 Comment(0)
S
2

Setting the state to Modified and SetValues only updates scalar properties of the Lecturer entity. Updating the Courses collection (which is not a scalar property) needs more work. You must handle the cases that a course could have been removed from the collection, a course could have been added or the scalar properties of a course could have been modified.

Also the way to update the collection depends on a course being dependent on the lecturer or not. Does the course need to be deleted from the database when it has been removed from the collection or does only the relationship between lecturer and course need to be removed? Does a new course need to be created when it has been added to the collection or does only a relationship need to be established?

If no courses must be deleted and no new courses be created the Update method could look like this:

public void UpdateLecturer(Lecturer lecturer)
{
    using(var db = new AcademicTimetableDbContext())
    {
        if (lecturer == null)
            return;

        var lecturerInDb = db.Lecturers
            .Include(l => l.Courses)
            .Single(l => l.Id_Lecturer == lecturer.Id_Lecturer);

        // Update lecturer
        db.Entry(lecturerInDb).CurrentValues.SetValues(lecturer);

        // Remove courses relationships
        foreach (var courseInDb in lecturerInDb.Courses.ToList())
            if (!lecturer.Courses.Any(c => c.Id_Course == courseInDb.Id_Course))
                lecturerInDb.Courses.Remove(courseInDb);

        foreach (var course in lecturer.Courses)
        {
            var courseInDb = lecturerInDb.Courses.SingleOrDefault(
                c => c.Id_Course == course.Id_Course);
            if (courseInDb != null)
                // Update courses
                db.Entry(courseInDb).CurrentValues.SetValues(course);
            else
            {
                // Add courses relationships
                db.Courses.Attach(course);
                lecturerInDb.Courses.Add(course);
            }
        }
    }

    db.SaveChanges();
}

Depending on the details of your scenario the correct solution might be slightly different.

Edit

If the courses in the lecturer.Courses collection have a reference to the lecturer (having a Lecturer navigation property maybe) you could have problems when you attach a course from this collection to the context because lecturerInDb is already attached and has the same key. You can try to change the last else block like so to solve the problem hopefully:

            else
            {
                // Add courses relationships
                var courseToAttach = new Course { Id_Course = course.Id_Course };
                db.Courses.Attach(courseToAttach);
                lecturerInDb.Courses.Add(courseToAttach);
            }
Skye answered 14/11, 2012 at 8:52 Comment(9)
It's almost that what I wanted to achieve. The problem is the exception during executing line db.Courses.Attach(course);. Exception: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key. I tried to remove line db.Courses.Attach(course); from code but then I have exception during executing db.SaveChanges(). Exception: The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects.Matthewmatthews
@patryk.beza: Hm, the last exception ("attached to different ObjectContext") indicates that your SelectedLecturer you pass into the method (and/or related courses) is attached to another context than the one you are creating in the method (in the using block). If so, you cannot use the method above at all. The method is designed for a detached scenario. I expected you are in such a scenario because you create a new context in the method. Can you maybe describe in your question how you load the SelectedLecturer and if you dispose the context it is loaded in or not?Skye
I updated my first post (Edit 2). There is detailed explanation how I loaded SelectedLecturer.Matthewmatthews
@patryk.beza: Can you try to add in your GetAllLecturers() method: dbb.Configuration.ProxyCreationEnabled = false; (as first line in the using block) and test again?Skye
It didn't help. ;/ Still An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key. in line db.Courses.Attach(course);.Matthewmatthews
@patryk.beza: Can you check the SelectedLecturer.Courses collection when you pass the SelectedLecturer into the UpdateLecturer method. Do the Course objects in that collection all have a different key value Id_Course?Skye
Yes, they have unique key values. Primary key Id_Course has Identity Increment = 1 (auto increment key).Matthewmatthews
@patryk.beza: I have a new idea :) See my Edit above.Skye
Wow! Amazing! You are my Hero. xD Thank you very much. I appreciate your effort & time to understand my (quite) complex problem. Thank you again! ;)Matthewmatthews

© 2022 - 2024 — McMap. All rights reserved.