2

I'm trying to remove a relationship with the following code:

                var serverVideo = InternalGetVideo(db, videoId);

                // get owning user
                var owner = InternalGetUser(db, serverVideo.UserId);

                // Add/remove video to user's upvotedVideos
                if (owner.UpvotedVideos.Contains(serverVideo))
                {
                    serverVideo.UpVotes--;
                    SaveChanges(db); // Works
                    owner.UpVotes--;
                    SaveChanges(db); // Works
                    owner.UpvotedVideos.Remove(serverVideo);
                    SaveChanges(db); // Breaks
                }

                SaveChanges(db);

But I always get this error:

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

I'm not trying to delete the video, I'm just trying to remove it from the user's UpvotedVideos property (it's there):

[Table("UserProfile")]
public class UserProfile
{
    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int UserId { get; set; }
    public string Username { get; set; }
    public int UpVotes { get; set; }
    public virtual List<Video> UpvotedVideos { get; set; }
    public string Email { get; set; }
}

Videos does not contain a reference to UserProfiles - it's a one-to-many relationship. Any idea what I'm doing wrong?

Videos class:

[DataContract]
public class Video
{
    [Key]
    [DataMember(IsRequired = false)]
    public int VideoId { get; set; }

    [DataMember(IsRequired = false)]
    public int UserId { get; set; }  // user that created the video, unrelated

    [Required]
    [DataMember]
    public virtual IList<Tag> Tags { get; set; }

}
RobVious
  • 12,685
  • 25
  • 99
  • 181
  • I think it might be helpful if you could post your `Video` class, at least the parts dealing with the relationship to `UserProfile` (the foreign key and the relationship property). – Steve Aug 23 '13 at 19:23
  • I'm assuming `db` is your context and is not disposed of throughout? – Steve Aug 23 '13 at 19:28
  • If you delete the server video before saving the changes, does it work? – Steve Aug 23 '13 at 19:31
  • @Steve - I can do db.Remove(serverVideo) instead and it works, but that's not what I'm trying to do - I'm just trying to remove the relationship. – RobVious Aug 23 '13 at 19:34
  • Apparently, if you're using self-tracking entities with EF4, you *might* be seeing a bug that's described here: http://stackoverflow.com/a/16003544/425871 – Steve Aug 23 '13 at 19:37
  • @Steve so simply removing a relationship in a basic one-to-many setup doesn't work out of the box? That's either tragic or I've given you the wrong information... isn't that what ORMs are for? – RobVious Aug 23 '13 at 19:44
  • Are you positive that EF doesn't think that `UserId` in `Video` is a foreign key? That would cause this issue. Try renaming it to something else and see if the problem goes away. – Omada Aug 23 '13 at 20:27
  • Well, yes - I've never experienced the problem you're having, though. The link I provided is apparently a bug and Microsoft apparently advises against using self-tracking entities. Did you try the solution at the link? – Steve Aug 23 '13 at 20:27
  • By the way, you might want to consider upgrading to EF5. I successfully did that a couple months ago... I reverse engineered my DB to a code-first model. The `DbContext` EF5 uses is *almost* compatible with `ObjectContext`, and the transition was relatively easy. – Steve Aug 23 '13 at 20:29

2 Answers2

1

Your comment in source code:

public int UserId { get; set; }  // user that created the video, unrelated

Unrelated? Most likely this is the foreign key for the one-to-many relationship in question because mapping conventions will detect it as the foreign key since it has the same name as the primary key property UserId in UserProfile. And because the property has a non-nullable type int EF will detect the relationship as required which means that no video can exist without having a user who upvoted it.

This is what the exception says when you remove the video from the UpvotedVideos collection which means that the relationship to the current user who upvoted the video is removed. You can do this but because every video must have a user who upvoted it you must assign the video either to another user or delete the video altogether.

Well, apparently you don't want that a video must have a user who upvoted it but instead a video can be without any upvotes. In this case the relationship must be optional. You can achieve this in several ways:

  • Break the conventional mapping by renaming the UserId property, for example

    public int CreatorId { get; set; }  // user that created the video, unrelated
    

    Now, this property is really unrelated and just a scalar property with no relationship at all. By default the relationship introduced by the UpvotedVideos collection is optional now because it doesn't have a required foreign key property on the other side.

  • Override the mapping convention with Fluent API:

    modelBuilder.Entity<UserProfile>()
        .HasMany(u => u.UpvotedVidoes)
        .WithOptional()
        .Map(m => m.MapKey("UpvoterId")); // <- FK column name
    
  • Introduce a second (nullable) property and define it as the foreign key with data annotations:

    [DataContract]
    public class Video
    {
        [Key]
        [DataMember(IsRequired = false)]
        public int VideoId { get; set; }
    
        [DataMember(IsRequired = false)]
        public int UserId { get; set; } // user that created the video, unrelated
    
        public int? UpvoterId { get; set; }
    
        [Required]
        [DataMember]
        public virtual IList<Tag> Tags { get; set; }
    }
    

    And in UserProfile:

    [ForeignKey("UpvoterId")]
    public virtual List<Video> UpvotedVideos { get; set; }
    

There are probably more variants that include adding navigation properties in Video, etc. But in all cases the relationship must be defined as optional to make your code working.

Side note: Shouldn't the relationship be many-to-many btw? A user can upvote many videos and a video can be upvoted by many users? But that's another question...

Slauma
  • 175,098
  • 59
  • 401
  • 420
  • Thank you so much Slauma. I'll give this a shot now. To answer your last question - I want it to be a one-to-many because I never need to know which users have upvoted a given video - I just need to know what videos a given user has upvoted. So i'm trying to keep the relationships as simple as possible. Does that make sense or am I wrong? – RobVious Aug 23 '13 at 20:43
  • @RobVious: You must use a many-to-many mapping with Fluent API then if you don't want a navigation property in `Video` refering to `User`s, like: `modelBuilder.Entity().HasMany(u => u.UpvotedVidoes).WithMany().Map(...);` The parameterless `WithMany()` says "no navigation property in `Video`" but it is still a many-to-many relationship. Defining it as one-to-many does not express your business logic correctly (that a video can be upvoted by *many* and not only by *one* user) and is wrong in my opinion. – Slauma Aug 23 '13 at 20:56
0

Instead of

owner.UpvotedVideos.Remove(serverVideo);

Try:

var video = owner.UpvotedVideos.Where(x => x == serverVideo).FirstOrDefault();
db.Videos.Remove(video); // notice, I'm removing it straight from my DB context
//so make sure you have Videos as a part of your DbContext
db.Entry(video).State = EntityState.Deleted; // tell the ChangeTracker you've removed somethings
db.SaveChanges();

When you try to remove right from the owner, EF doesn't allow nulls within navigation properties. Also, when I tried making Foreign keys optional - I saw that they still remain in the DB. I didn't need all of those orphan records in there, so I used this solution.

Arman Bimatov
  • 1,789
  • 4
  • 24
  • 31