简体   繁体   中英

C# Entity Framework: creating one-to-many relationship

This issue has been driving me nuts for days, and frankly at the moment I feel like I'm clueless because I've been searching for so long and tried so many things and none of them work. So I thought it would be time to ask a question.

I'm trying to create a small console app that works with workout moves (Move) as primary entity. However, as part of the app I also want users to be able leave a rating and intensity for each workout in an entity called MoveRating which collects mentioned ratings whenever a Move is fetched.

So this lead me to create the following set-up:

Move.cs

    public class Move
    {
        public int Id { get; set; }
        public string MoveName { get; set; }
        public string MoveDescription { get; set; }
        public int SweatRate { get; set; }
        public ICollection<MoveRating> MoveRating { get; set; }
    }

MoveRating.cs

    public class MoveRating
    {
        public int Id { get; set; }
        public Move Move { get; set; }
        public double Rating { get; set; }
        public double Intensity { get; set; }
    }

Now I know I'm supposed to do something in my DbContext to make this happen, I've been trying stuff like the following:

DbContext (only the OnModelCreating part)

      protected override void OnModelCreating(ModelBuilder builder)
          {
            builder.Entity<MoveRating>().HasOne(m => m.Move).WithMany(a => a.MoveRating);
            builder.Entity<Move>().HasMany(m => m.MoveRating).WithOne(g => g.Move);
          }

I know it shouldn't be like this, but whatever example I do try to follow it just doesn't work. I've tried stuff like:

    builder.Entity<MoveRating>().HasOne(b => b.Move).WithMany(m => m.MoveRating).HasForeignKey(m => m.MoveId);

Or

    builder.Entity<Move>().HasMany(m => m.MoveRating).WithRequired(m => m.MoveRating);

I feel like one of these should work. But whatever I try to do I can't seem to get it to work. It will give me messages like "MoveRating does not contain a definition for MoveId" or "CollectionNavigationBuilder<Move, MoveRating> does nto contain a definition for WithRequired."

Can someone please help me get over this issue? I'm losing my mind.

Create a new directory, call it whatever you want.

Inside the directory, create a new mvc application with the following command on the command line:

dotnet new mvc --auth Individual  

--auth Individual part ensures you have entity framework libraries referenced.

Open your application code in your text editor.

To create a one to many relationship in EF Core with one Move to many MoveRatings, your entities would look like this (notice the [Key] annotation):

public class Move
{
    [Key]
    public int Id { get; set; }
    public string MoveName { get; set; }
    public string MoveDescription { get; set; }
    public int SweatRate { get; set; }
    public List<MoveRating> MoveRating { get; set; }
}

public class MoveRating
{
    [Key]
    public int Id { get; set; }
    public Move Move { get; set; }
    public double Rating { get; set; }
    public double Intensity { get; set; }
}

In your DbContext, you will have two properties like so:

public DbSet<Move> Moves { get; set; }
public DbSet<MoveRating> MoveRatings { get; set; }

Make sure your ConnectionString is pointing to the correct server and database. You will find this string, in most cases, in appsettings.js .

Once everything looks good, you will execute the following in the command line (make sure you're inside your project directory):

dotnet ef migrations add InitialCreate

When that runs successfully, you can see a Migrations folder and there you will see a file with a name that starts with some numbers followed by InitialCreate.cs. You can open it and see that it looks something like this:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.CreateTable(
        name: "Moves",
        columns: table => new
        {
            Id = table.Column<int>(type: "INTEGER", nullable: false)
                .Annotation("Sqlite:Autoincrement", true),
            MoveName = table.Column<string>(type: "TEXT", nullable: true),
            MoveDescription = table.Column<string>(type: "TEXT", nullable: true),
            SweatRate = table.Column<int>(type: "INTEGER", nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_Moves", x => x.Id);
        });

    migrationBuilder.CreateTable(
        name: "MoveRatings",
        columns: table => new
        {
            Id = table.Column<int>(type: "INTEGER", nullable: false)
                .Annotation("Sqlite:Autoincrement", true),
            MoveId = table.Column<int>(type: "INTEGER", nullable: true),
            Rating = table.Column<double>(type: "REAL", nullable: false),
            Intensity = table.Column<double>(type: "REAL", nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_MoveRatings", x => x.Id);
            table.ForeignKey(
                name: "FK_MoveRatings_Moves_MoveId",
                column: x => x.MoveId,
                principalTable: "Moves",
                principalColumn: "Id",
                onDelete: ReferentialAction.Restrict);
        });

    migrationBuilder.CreateIndex(
        name: "IX_MoveRatings_MoveId",
        table: "MoveRatings",
        column: "MoveId");
}

After you're happy, execute the following in the command line:

dotnet ef database update

That should create the relationship in your database.

Notes

If you don't have EF tools installed, you can install them by executing:

dotnet tool install --global dotnet-ef

in your command line.

References

EF Core - Relationships: https://docs.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key

EF Core - Tools Reference: https://docs.microsoft.com/en-us/ef/core/cli/dotnet

I'd recommend you to define all relations with the help of ModelBuilder as less as possible, because you need to check out your configuration file each time, when you to rewind your database structure. Still it's up to you.

Basically, what you need is just add an int MoveId property in MoveRating class, remove your config and that's all. It will fetch Move table because of names convention.

If your database already exists and you are not using Code-first + migrations to update it, ensure that your MoveRating table has a MoveId column. It will need one.

Next: for your object definitions I recommend always declaring the navigation properties as virtual . This ensures that lazy loading will be available as a fall-back to avoid things like null reference exceptions. Collections should be auto-initialized so they are ready to go when creating new parent entities as well:

public class Move
{
    [Key]
    public int Id { get; set; }
    public string MoveName { get; set; }
    public string MoveDescription { get; set; }
    public int SweatRate { get; set; }
    public virtual ICollection<MoveRating> MoveRating { get; set; } = new List<MoveRating>();
}

public class MoveRating
{
    [Key]
    public int Id { get; set; }
    public virtual Move Move { get; set; }
    public double Rating { get; set; }
    public double Intensity { get; set; }
}

Next will be mapping the association between Move and MoveRating. You could add a MoveId to your MoveRating entity to serve as the FK to Move:

public class MoveRating
{
    [Key]
    public int Id { get; set; }
    [ForeignKey("Move")]
    public int MoveId { get; set; }
    public virtual Move Move { get; set; }
    public double Rating { get; set; }
    public double Intensity { get; set; }
}

This is simple and should get you going, but I generally advise against doing this as it creates two sources of truth for the Move reference. (moveRating.MoveId and moveRating.Move.Id) Updating the FK does not automatically update the reference to "Move" and vice-versa. YOu can leverage shadow properties to specify the FK without declaring the FK field in the entity. Given the error you got with HasRequired it looks like you are using EF Core:

protected override void OnModelCreating(ModelBuilder builder)
{
    builder.Entity<Move>()
        .HasMany(m => m.MoveRating)
        .WithOne(g => g.Move)
        .HasForeignKey("MoveId");
}

This is where your MoveRating entity does not have a field for "MoveId" declared. EF will create a shadow property to represent the FK mapping behind the scenes.

When using modelBuilder or EntityTypeConfigurations, only map out relationships from one side, not both sides. For instance if you create a HasMany from Move to MoveRating, do not add another mapping from MoveRating back to Move. (It isn't required and it can lead to bugs)

Lastly, when reading your Move and it's associated MoveRatings collections, if you know you are going to need the ratings, eager load them when you read the Move entity. This generally means avoiding EF methods like Find in favour of Linq methods like Single .

var move = context.Moves
    .Include(m => m.MoveRatings)
    .Single(m => m.MoveId == moveId);

This would fetch the move /w it's collection of Ratings in one SQL call. By declaring the MoveRatings as virtual provides a safety net that if you forget to use Include on a Move, provided that its DbContext reference is still alive, the MoveRatings can be pulled with a lazy load call when accessed. (Something to watch out for and avoid whenever possible, but better than triggering a null reference exception)

Took me a number of days, but the answer to the problem was stupidly simple...

It didn't take the MoveRatings because I tried to access them outside of the data access layer where I defined my DbContext.. That was all..

Don't know if I should keep this open for visisbility (since I couldn't find this answer anywhere) or if I should delete

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM