0

I have an Index action in my MVC project that accepts an integer as a parameter which represents the ID of a movie title in my SQL Server database. If the ID is already in the database, a ViewModel is populated with the movie title's data and passed back to the view. If the ID is a new one, a new movie Title object is created, populated, added to the context, and saved to the database. The ID is not automatically assigned by the database. Action code below (some code removed/omitted for brevity and clarity):

    [HttpGet]
    public async Task<ActionResult> Index(int? i)
    {
        if (i.HasValue)
        {
            var movieViewModel = new MovieViewModel();
            using (var context = new someEntities())
            {
                //check if this id already exists in the DB
                var matchedTitle = await context.Titles.FindAsync(i.Value);
                if (matchedTitle != null && matchedTitle.Category == "movie")
                {                     
                    //matchedTitle already in the DB.. 
                    //..so populate movieViewModel with matchedTitle
                }
                else
                {
                    //matchedTitle not in the DB, generate a new Title item
                    var newTitle = TitleHelper.GenerateNewTitle(i.Value, /* .. */);                     
                    //add and save
                    context.Titles.Add(newTitle);                        
                    await context.SaveChangesAsync();                        
                    //code omitted for brevity
                }
            }
            Response.StatusCode = (int)HttpStatusCode.OK;
            return View(movieViewModel);
        }
        else
        {
            return View();
        }
    }

This works fine but will occasionally produce the following error during high traffic:

Violation of PRIMARY KEY constraint 'PK_Title_Title_Id'. Cannot insert duplicate key in object 'dbo.Title'. The duplicate key value is (0147852). The statement has been terminated.

I have not been able to reproduce the error on purpose. If I hit a refresh on that view, the error goes away and the view loads normally. Am I missing a check or is this a race condition scenario? I would have thought that EF would be able to handle this.

Update:

Some additional info as requested in the comments:

  • How do I generate IDs?

    They IDs are generated based on a movie's respective IMDb ID.

  • How do I generate a new title?

    public static Title GenerateNewTitle(int id, //other properties)
    {           
        Title newTitle = new Title
        {
            //property population here
        }
        return newTitle;
    }       
    
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
trashr0x
  • 6,457
  • 2
  • 29
  • 39
  • this condition `matchedTitle.Category == "movie"` might be the issue, can you please elaborate about this line? – Mox Shah Feb 17 '15 at 13:07
  • You've removed code which I think may pinpoint the problem – ediblecode Feb 17 '15 at 13:08
  • @jumpingcode the removed code is pretty much population of the properties of the `movieViewModel` – trashr0x Feb 17 '15 at 13:10
  • The problem could be in your code responsible for ID generation. – Michal Klouda Feb 17 '15 at 13:11
  • No harm in including it! Also, could you show the internals of `GenerateNewTitle` – ediblecode Feb 17 '15 at 13:12
  • try debug and verify if `TitleHelper.GenerateNewTitle(/* .. */)` this line assign 0 to ID column. – Mox Shah Feb 17 '15 at 13:12
  • You should make `Category` an `enum`. In fact, this would be a good candidate for codereview.stackexchancge.com – ediblecode Feb 17 '15 at 13:13
  • 1
    In-case of edit in where `category = show`, will throw an error because `var matchedTitle = await context.Titles.FindAsync(i.Value)` will find a record for `i.value` id, but `matchedTitle.Category == "movie"` won't match as this is a type of `show` category, and `TitleHelper.GenerateNewTitle(/* .. */)` this function must be assigning PK ID from `i.value` only and hence it will throw PK violation error. – Mox Shah Feb 17 '15 at 13:15
  • hi guys, see my edits as requested – trashr0x Feb 17 '15 at 13:22
  • 1
    Are these errors coming about in your own testing when just you are using the system or is the system being used by multiple people? Because there's a classic race here if two users hit this function at about the same time for the same movie (both check, both find no movie, both attempt to create the movie) – Damien_The_Unbeliever Feb 17 '15 at 13:32
  • @jumpingcode Once the code works as it should, and sometime later on, perhaps. But please, [be careful recommending it](http://meta.stackoverflow.com/questions/253975/be-careful-when-recommending-code-review-to-askers) – Simon Forsberg Feb 17 '15 at 13:46

2 Answers2

1

This was solved a while ago but seeing that no answer has been provided as of yet I provide a solution below for future reference.

The problem was, as suggested by Damien_The_Unbeliever in the comments of the OP, a typical race condition due to semi-complete, untested code of a quick'n'dirty solution (error 2627), albeit only occurring during high traffic. Checking if the object already exists before inserting is not a solution, unless you lock the table prior to the check. Otherwise, someone might insert the object after the check and before the insertion.

I'm currently overriding OnException of System.Web.Mvc.Controller to handle my exceptions, but "JFDI" is a quick good way to go about this:

using System.Data.Entity.Infrastructure;

try
{
    await context.SaveChangesAsync();
}
catch(DbUpdateException ex)
{
    var sqlException = ex.InnerException.InnerException as SqlException;
    if (sqlException != null && sqlException.Number == 2627)
    {
        //violation of primary key constraint has occurred here, act accordingly
        //e.g. pass the populated viewmodel back to the view and return
    }
}
Community
  • 1
  • 1
trashr0x
  • 6,457
  • 2
  • 29
  • 39
0

In-case of edit in where category = show, will throw an error because var matchedTitle = await context.Titles.FindAsync(i.Value) will find a record for i.value id, but matchedTitle.Category == "movie" won't match as this is a type of show category, and TitleHelper.GenerateNewTitle(/* .. */) this function must be assigning PK ID from i.value only and hence it will throw PK violation error.

Try bellow code

    var newTitle = TitleHelper.GenerateNewTitle(i.Value, /* .. */);
    if(newTitle.Title_Id > 0)
{
 // Query to update your record
}
else
{
  //add and save
  context.Titles.Add(newTitle);                        
  await context.SaveChangesAsync();
}
Mox Shah
  • 2,967
  • 2
  • 26
  • 42
  • The IDs are based on IMDb so uniqueness is already there, since both movies and shows exist in the same `ttXXXXXXX` range. Also, as you can see in the error, the error occurred on positive integer ID `0147852`. The `Category == "movie"` check is indeed unnecessary, so I'll remove it. – trashr0x Feb 17 '15 at 13:51
  • Yes, if you remove that check then It won't throw any error! – Mox Shah Feb 17 '15 at 13:52
  • I'm afraid this wasn't a scenario of trying to insert a movie where it's ID already existed in the database but had it's `Category` column set to `"show"`. But still, good suggestion :) – trashr0x Feb 17 '15 at 13:55