Reputation: 5846
I am getting an error when I perform an Update-Database in PM Console. I understand why, but given my needs I'm not sure how to best overcome it. I hope somebody can point me in the right direction.
I have 2 tables, one for countries and one for states. Usually I would use the Id for a primary key, but my client has asked that I make associations human readable. For example, instead of a user having a value for their country as 1, they want it to be "USA". Instead of a value of 14 for their state they way to see "CA" or "TX", etc.
My Migration Configuration.cs contains a seed routine to add countries and states.
When I run Update-Database, I get the following error:
Running Seed method. System.Data.Entity.Infrastructure.DbUpdateException: Unable to determine the principal end of the 'XXX_XXX_XXX.Models.State_Country' relationship. Multiple added entities may have the same primary key. ---> System.Data.Entity.Core.UpdateException: Unable to determine the principal end of the 'XXXX_XXXX_XXXX.Models.State_Country' relationship. Multiple added entities may have the same primary key.
My ViewModel contains:
namespace XXXX_XXXX_XXXX.Models
{
public class Country
{
[Key]
public string CountryCode { get; set; }
public int CountryId { get; set; }
public string CountryName { get; set; }
public virtual ICollection<State> States { get; set; }
}
public class State
{
[Key]
public string StateCode { get; set; }
public int StateId { get; set; }
public string StateName { get; set; }
[ForeignKey("Country")]
public string CountryCode { get; set; }
public virtual Country Country { get; set; }
}
}
My seed routine looks like this:
protected override void Seed(ProgramDbContext context)
{
// Initialize Countries
context.Countries.AddOrUpdate(c => c.CountryCode,
new Country { CountryId = 1,
CountryName = "United States",
CountryCode = "USA" });
context.Countries.AddOrUpdate(c => c.CountryCode,
new Country { CountryId = 2,
CountryName = "Canada",
CountryCode = "CAN" });
{ ... }
// Initialize States
context.States.AddOrUpdate(c => c.StateId,
new State { StateId = 1,
StateName = "Iowa",
StateCode = "IA",
CountryCode = "USA" });
context.States.AddOrUpdate(c => c.StateId,
new State { StateId = 2,
StateName = "Illinois",
StateCode = "IL",
CountryCode = "USA" });
context.States.AddOrUpdate(c => c.StateId,
new State { StateId = 3,
StateName = "California",
StateCode = "CA",
CountryCode = "USA" });
{ ... }
}
I DO understand that I am getting the error because CountryCode doesn't contain unique values for every column. If I set the CountryId to the Key and change the Foreign Key for States to CountryId I don't get the error, but then it seems I have to perform a lookup on the Id fields to get the CountryCode whenever I need it.
I also foresee that it's possible and even likely that the StateCode will eventually have a duplicate, though it doesn't currently.
Is it possible to have 2 [Key] attributes in a single table using ViewModel coding? I couldn't get that to work.
Do I need to place the [Key]
attribute on the Id columns of each of these tables and then look-up the CountryCode and StateCode each time I need to update a user profile or create a dropdown list? Btw, I've created custom Identity fields for CountryCode and StateCode.
How should my ViewModel for Country and State be coded so that I can conveniently have my user profiles show "CA, USA" instead of 3, 1 - for example?
EDIT
@David offered to look at my View code for the Country and State dropdowns. I populate the State dropdown using jQuery
I populate Model.Countries in my controller with:
public IEnumerable<SelectListItem> GetCountryList()
{
var countries = new SelectList(dbLocations.Countries, "CountryCode", "CountryName").ToList();
countries.Insert(0, (new SelectListItem { Text = "Select Country", Value = "-1" }));
countries.Insert(1, (new SelectListItem { Text = "United Sates", Value = "54" }));
countries.Insert(2, (new SelectListItem { Text = "Canada", Value = "13" }));
countries.Insert(3, (new SelectListItem { Text = "Mexico", Value = "12" }));
countries.Insert(4, (new SelectListItem { Text = "Brazil", Value = "36" }));
countries.Insert(5, (new SelectListItem { Text = "------------------------", Value = "-1" }));
IEnumerable<SelectListItem> countrylist =
countries.Select(m => new SelectListItem()
{
Text = m.Text,
Value = m.Value
});
return countrylist;
}
An example of how I use in a View:
<div class="form-group">
@Html.LabelFor(m => m.CountryId, new { @class = "col-md-3 control-label" })
<div class="col-md-9">
@Html.DropDownListFor(
x => x.CountryId,
new SelectList(Model.Countries, "Value", "Text"),
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.StateId, new { @class = "col-md-3 control-label" })
<div class="col-md-9">
<div id="stateid_select">
<select id="StateId" class="form-control" name="StateId"></select>
</div>
</div>
</div>
jQuery for Country change (retrieves states):
<script type="text/javascript">
// Populate State/Province dropdown on Country select
$(function () {
$('#CountryCode').change(function () {
$.getJSON('/Account/StateList/' + $('#CountryCode').val(), function (data) {
var cnt = 0;
var items = '<option>Select a State/Province</option>';
$.each(data, function (i, state) {
items += "<option value='" + state.Value + "'>" + state.Text + "</option>";
cnt = cnt + 1;
});
// Hide SELECT and show TEXT field if no state/provinces to choose from.
// Else show SELECT and hide TEXT field.
if (!cnt == 0) {
$('#stateprovince_select select').html(items).show().attr("disabled", false).removeClass("hide");
$('#stateprovince_text input[type="text"]').hide().attr("disabled", true).addClass("hide");
} else {
$('#stateprovince_select select').html('').hide().attr("disabled", true).addClass("hide");
$('#stateprovince_text input[type="text"]').show().attr("disabled", true).removeClass("hide");
$('#StateProvinceCode').hide();
}
});
});
});
</script>
jQuery calls this routine in Controller:
[AllowAnonymous]
[AcceptVerbs(HttpVerbs.Get)]
public JsonResult StateList(int countryid)
{
var state = from s in dbLocations.States
where s.CountryId == countryid
select s;
return Json(new SelectList(state.ToArray(), "StateId", "StateName"), JsonRequestBehavior.AllowGet);
}
May be more than you needed to see. I made some changes to reflect the ModelView changes, though I haven't been able to run the app yet - still making changes. The jQuery needs some cleaning up, I know. ;)
Upvotes: 0
Views: 142
Reputation: 119166
You should leave the Key
index on the Id column but add a unique constraint to the CountryCode
. This has the following benefits:
You relationships are all using the integer Id columns which will perform better than joining on strings.
The CountryCode will now not allow duplicate entries.
The CountryCode can be shown to users and hides away the fact your database is linking with integers.
So now your models look like this:
public class Country
{
[Key]
public int Id { get; set; }
[Index("UX_Country_CountryCode", IsUnique = true)]
[MaxLength(10)]
public string CountryCode { get; set; }
public string CountryName { get; set; }
public virtual ICollection<State> States { get; set; }
}
public class State
{
[Key]
public int Id { get; set; }
[Index("UX_State_StateCode", IsUnique = true)]
[MaxLength(10)]
public string StateCode { get; set; }
public string StateName { get; set; }
[ForeignKey("Country")]
public int CountryId { get; set; }
public virtual Country Country { get; set; }
}
If you want to be able to have the same state code in different countries, extend the unique constraint like this:
public class State
{
[Key]
public int Id { get; set; }
[Index("UX_State_StateCodeCountryId", IsUnique = true, Order = 1)]
[MaxLength(10)]
public string StateCode { get; set; }
public string StateName { get; set; }
[ForeignKey("Country")]
[Index("UX_State_StateCodeCountryId", IsUnique = true, Order = 0)]
public int CountryId { get; set; }
public virtual Country Country { get; set; }
}
Upvotes: 1