Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

No new many to many connections are made in the database when saving an object in EF

I have a problem with my code where I try to save a many to many connection between two objects, but for some reason it doesn't get saved.

We used the code first method to create our database, in our database we have the following entities where this problem is about:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<ProductTag> ProductTags { get; set; }
}
public class ProductTag
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

The table ProductTagProducts got automatically created, which is of course just a connection table between the two.

Now creating products works fine. We can just run the following and it will create the connnections in the ProductTagProducts table:

Product.ProductTags.Add(productTag);

To make sure no duplicate tasks are in the database, we handle the saving for it ourselves. The productTag always contains a product tag with an existing ID.

The problem occurs when we want to edit the same or another product. There are existing tags for the product. And we use the following process to save it:

List<ProductTag> productTags = new List<ProductTag>();
string[] splittedTags = productLanguagePost.TagList.Split(',');
foreach (string tag in splittedTags) {
    ProductTag productTag = new ProductTag();
    productTag.Name = tag;
    productTags.Add(productTagRepository.InsertAndOrUse(productTag));
}

We split the tags by comma, that's how it is received from the HTML element. Then we define a new entity for it and use InsertAndOrUse to determine if the tag already existed. If the tag already existed, it returns the same entity but with the ID filled in, if it did not exist yet it adds the tag to the database, and then also returns the entity with ID. We create a new list to be sure that the product doesn't have duplicate Id's in there (I have tried it with adding it to the product's existing tag list directly, same result).

product.ProductTags = productTags;
productRepository.InsertOrUpdate(product);
productRepository.Save();

Then we set the list to ProductTags and let the repository handle the insert or update, of course, an update will be done. Just in case, this is the InsertOrUpdate function:

public void InsertOrUpdate(Product product) {
    if (product.Id == default(int)) {
        context.Products.Add(product);
    } else {
        context.Entry(product).State = EntityState.Modified;
    }
}

The save method just calls the context's SaveChanges method. When I edit the product, and add another tag it doesn't save the new tag. However, when I set a breakpoint on the save function I can see that they are both there: enter image description here

And when I open the newly added tag 'Oeh-la-la' I can even refer back to the product through it: enter image description here

But when the save happens, which succeeds with all other values, there are no connections made in the ProductTagProducts table. Maybe it is something really simple, but I am clueless at the moment. I really hope that someone else can give a bright look.

Thanks in advance.

Edit: As requested the ProductTag's InsertAndOrUse method. The InsertOrUpdate method it calls is exactly the same as above.

public ProductTag InsertAndOrUse(ProductTag productTag)
{
    ProductTag resultingdProductTag = context.ProductTags.FirstOrDefault(t => t.Name.ToLower() == productTag.Name.ToLower());

    if (resultingdProductTag != null)
    {
        return resultingdProductTag;
    }
    else
    {
        this.InsertOrUpdate(productTag);
        this.Save();
        return productTag;
    }
}
like image 973
Chris Avatar asked Aug 01 '13 09:08

Chris


1 Answers

You have to know that this line...

context.Entry(product).State = EntityState.Modified;

...has no effect on the state of a relationship. It just marks the entity product being passed into Entry as Modified, i.e. the scalar property Product.Name is marked as modified and nothing else. The SQL UPDATE statement that is sent to the database just updates the Name property. It doesn't write anything into the many-to-many link table.

The only situation where you can change relationships with that line are foreign key associations, i.e. associations that have a foreign key exposed as property in the model.

Now, many-to-many relationships are never foreign key associations because you cannot expose a foreign key in your model since the foreign keys are in the link table that doesn't have a corresponding entity in your model. Many-to-many relationships are always independent associations.

Aside from direct manipulations of relationship state entries (which is rather advanced and requires to go down to the ObjectContext) independent associations can only be added or deleted using Entity Framework's change tracking. Moreover you have to take into account that a tag could have been removed by the user which requires that a relationship entry in the link table must be deleted. To track such a change you must load all existing related tags for the given product from the database first.

To put all this together you will have to change the InsertOrUpdate method (or introduce a new specialized method):

public void InsertOrUpdate(Product product) {
    if (product.Id == default(int)) {
        context.Products.Add(product);
    } else {
        var productInDb = context.Products.Include(p => p.ProductTags)
            .SingleOrDefault(p => p.Id == product.Id);

        if (productInDb != null) {
            // To take changes of scalar properties like Name into account...
            context.Entry(productInDb).CurrentValues.SetValues(product);

            // Delete relationship
            foreach (var tagInDb in productInDb.ProductTags.ToList())
                if (!product.ProductTags.Any(t => t.Id == tagInDb.Id))
                    productInDb.ProductTags.Remove(tagInDb);

            // Add relationship
            foreach (var tag in product.ProductTags)
                if (!productInDb.ProductTags.Any(t => t.Id == tag.Id)) {
                    var tagInDb = context.ProductTags.Find(tag.Id);
                    if (tagInDb != null)
                        productInDb.ProductTags.Add(tagInDb);
                }
        }
    }

I was using Find in the code above because I am not sure from your code snippets (the exact code of InsertAndOrUse is missing) if the tags in the product.ProductTags collection are attached to the context instance or not. By using Find it should work no matter if the they are attached or not, potentially at the expense of a database roundtrip to load a tag.

If all tags in product.ProductTags are attached you can replace ...

                    var tagInDb = context.ProductTags.Find(tag.Id);
                    if (tagInDb != null)
                        productInDb.ProductTags.Add(tagInDb);

... just by

                    productInDb.ProductTags.Add(tag);

Or if it's not guaranteed that they are all attached and you want to avoid the roundtrip to the database (because you know for sure that the tags at least exist in the database, if attached or not) you can replace the code with:

                    var tagInDb = context.ProductTags.Local
                        .SingleOrDefault(t => t.Id == tag.Id);
                    if (tagInDb == null) {
                        tagInDb = tag;
                        context.ProductTags.Attach(tagInDb);
                    }
                    productInDb.ProductTags.Add(tagInDb);
like image 106
Slauma Avatar answered Sep 23 '22 05:09

Slauma