Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect simple join tables in reverse engineering and create many-to-many relationships #22475

Closed
Tracked by #24106 ...
ajcvickers opened this issue Sep 10, 2020 · 53 comments
Assignees
Labels
area-many-to-many area-scaffolding closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. ef6-parity type-enhancement
Milestone

Comments

@ajcvickers
Copy link
Member

No description provided.

@ajcvickers
Copy link
Member Author

Notes from triage:

  • The pri-zero here is that a database created by EF6 with many-to-many relationships should reverse-engineer to EF Core with many-to-many relationships. Specifically, the join table must contain only columns for the two FKs and these columns must also form the primary key. If the table has a separate PK or any payload columns, then it should not be considered many-to-many.
  • Meeting the EF6 scenario should also mean that simple, fully by-convention many-to-many relationships created by EF Core will round-trip.
  • It's too late to do this for EF Core 5.0.

@ErikEJ Might you consider doing this in the EF Core Power Tools? That would make something available for 5.0. Also, in the future, if you want to do the PR for here, that would be awesome! 😁

@ajcvickers ajcvickers added this to the Backlog milestone Sep 11, 2020
@ErikEJ
Copy link
Contributor

ErikEJ commented Sep 11, 2020

@ajcvickers I assume that would require changes to code generation, then?

@ajcvickers
Copy link
Member Author

@ErikEJ Yes, it probably would. @bricelam?

@ErikEJ
Copy link
Contributor

ErikEJ commented Sep 11, 2020

Ok - I have already reluctantly pulled in some Internal code anyway. Happy to take a PR for this!!

@smitpatel
Copy link
Contributor

It would probably require some changes to code gen. Currently there is no processing of SkipNavigations in code gen at all. I think best place to identify and add skip navigation would be RelationalScaffoldingModelFactory (now we are deep in internal territory lol) to add skip navs to model and code gen will handle it as such.

@ErikEJ
Copy link
Contributor

ErikEJ commented Sep 12, 2020

Hmmm.. where would I specify the table name (since it most likely will not match convention)

Assume in fluent config, but how, since there is no EntityType to refer to??

@smitpatel
Copy link
Contributor

modelBuilder.Entity<One>().HasMany(e => e.TwoSkip).WithMany(e => e.OneSkip).UsingEntity(e => e.ToTable("tableName"));

EF will fill in actual entity type appropriately, whatever that type would be by convention. (Likely Dictionary<string, object>)
(Untested code)

@bmoteria
Copy link

@smitpatel The provided solution doesn't seem to work in my scenario as the table "FeatureVehicle" contains two columns which has underscore ("Feature_ID" and "Vehicle_VehicleID"). (cc @ajcvickers)

When query is executed it throws following exception:

Microsoft.EntityFrameworkCore.Query: Error: An exception occurred while iterating over the results of a query for context type '<PRODUCT>.Data.Context.<PRODUCT>DbContext'.
Microsoft.Data.SqlClient.SqlException (0x80131904): Invalid column name 'FeaturesID'.
Invalid column name 'VehiclesID'.
Invalid column name 'VehiclesID'.
Invalid column name 'VehiclesID'.
Invalid column name 'FeaturesID'.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
   at Microsoft.Data.SqlClient.SqlDataReader.get_MetaData()
   at Microsoft.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString, Boolean isInternal, Boolean forDescribeParameterEncryption, Boolean shouldCacheForAlwaysEncrypted)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean isAsync, Int32 timeout, Task& task, Boolean asyncWrite, Boolean inRetry, SqlDataReader ds, Boolean describeParameterEncryptionRequest)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String method)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior)
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
   at System.Data.Common.DbCommand.ExecuteReader()
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.InitializeReader(DbContext _, Boolean result)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
ClientConnectionId:76b39b9f-06db-43a9-ae78-d7820dd60584

Here are my entities:

public partial class Vehicle
{
     ....
     public ICollection<Feature> Features { get; set; }
}

public partial class Feature
{
     ....
     public ICollection<Vehicle> Vehicles { get; set; }
}

@smitpatel
Copy link
Contributor

@bmoteria - Your issue is not related to the issue you are posting on. Please avoid off-topic discussions. Your column names in the join entity does not match what is generated conventionally, so you will need to configure them manually using HasColumnName.

@cgountanis
Copy link

cgountanis commented Nov 25, 2020

I am ready for this change, we use database first and it would be great if it automatically did the Many to Many so something like context.Posts.Include(e => e.Tags) would work automatically.

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id])
);

CREATE TABLE [Tag] (
    [Id] int NOT NULL IDENTITY,
    [Text] nvarchar(max) NULL,
    CONSTRAINT [PK_Tag] PRIMARY KEY ([Id])
);

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tag_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tag] ([Id]) ON DELETE CASCADE
);

CREATE INDEX [IX_PostTag_TagsId] ON [PostTag] ([TagsId]);

@ajcvickers
e.g. Scaffold-DbContext "Server=localhost;Database=XXX;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Context Context -ContextDir Models -Force

s-e-r-O added a commit to s-e-r-O/the-great-pizza-test that referenced this issue Mar 21, 2021
…Specifications.

Because the support for scaffolding many-to-many relationships from the database is not yet added (dotnet/efcore#22475).
s-e-r-O added a commit to s-e-r-O/the-great-pizza-test that referenced this issue Mar 21, 2021
…Specifications.

Because the support for scaffolding many-to-many relationships from the database is not yet added (dotnet/efcore#22475).
@ErikEJ
Copy link
Contributor

ErikEJ commented Sep 17, 2021

Power Tools simply uses the default CSharpDbContextGenerator.cs, but it hooks into the DatabaseModel generation, and manipulates that in flight.

I was looking for a simple way to exclude a table from many-to-many, not include, to allow users to opt out on a per table basis.

@smitpatel
Copy link
Contributor

Looking at power tools code, I see that you already have one derived class for RelationalScaffoldingModelFactory, if you are fine overriding it and copying some code then overriding VisitForeignKeys would get you that in 6.0. Basically you use the code without skip navs (how it was in 5.0) to skip M2M join table. Once we don't add skip navigations there, rest of the code should generate code for it like a normal entity.

@ErikEJ
Copy link
Contributor

ErikEJ commented Sep 17, 2021

@smitpatel Perfect! I will consider that if customers ask for it!

@ajcvickers
Copy link
Member Author

@ErikEJ So it looks like right now you don't need us to add/change things for you?

@ErikEJ
Copy link
Contributor

ErikEJ commented Sep 20, 2021

@ajcvickers Correct!

@Zapnologica
Copy link

Not to sure how we can vote for this feature, however we have just migrated to .net 5 and have a very large database with a large number of many to many joins, and the current scaffolding has not mapped them correctly at all.

To go and manually do all the Join entities is gonna take ages.

@ErikEJ
Copy link
Contributor

ErikEJ commented Oct 29, 2021

@Zapnologica This is implemented in EF Core 6!

@Zapnologica
Copy link

Zapnologica commented Oct 29, 2021 via email

@ErikEJ
Copy link
Contributor

ErikEJ commented Oct 29, 2021

@Zapnologica it will happen within two weeks!

@ajcvickers ajcvickers modified the milestones: 6.0.0-rc1, 6.0.0 Nov 8, 2021
@hgsenger
Copy link

Hi, although this is a nice feature we have the following scenario, where we have some trouble with it:

  • Table 'A'
  • Table 'B'
  • MappingTable1 A <-> B (m:n)
  • MappingTable2 A <-> B (m:n)

Scaffolding the database context fails in this case with the error message that the navigation property Table 'B' cannot be added twice to Table 'A'.

It would be nice if the navigation property names could be based on the intersection table name(s) to circumvent this issue.

@AndriySvyryd
Copy link
Member

@hgsenger This will be fixed in 6.0.1

@glucklichman
Copy link

Hi, I take a simple many-to-many relationship. After scaffolding I will take one entity with two Reference navigation and two entities without collection navigation. As I read the documentation, the two entities mave to include the collection navigation.

public partial class PersonTrainingCourse
{
        public int PersonId { get; set; }
        public int CourseId { get; set; }

        public virtual TrainingCourse Course { get; set; }
        public virtual Person Person { get; set; }
}

And dbContext includes only one-to-many relationships

modelBuilder.Entity<PersonTrainingCourse>(entity =>
            {
                ...

                entity.HasOne(d => d.Course)
                    .WithMany()
                    .HasForeignKey(d => d.CourseId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Person<>TrainingCourse_TrainingCourse");

                entity.HasOne(d => d.Person)
                    .WithMany()
                    .HasForeignKey(d => d.PersonId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Person<>TrainingCourse_Person");
            });

@moritzinio
Copy link

Hi, I take a simple many-to-many relationship. After scaffolding I will take one entity with two Reference navigation and two entities without collection navigation. As I read the documentation, the two entities mave to include the collection navigation.

public partial class PersonTrainingCourse
{
        public int PersonId { get; set; }
        public int CourseId { get; set; }

        public virtual TrainingCourse Course { get; set; }
        public virtual Person Person { get; set; }
}

And dbContext includes only one-to-many relationships

modelBuilder.Entity<PersonTrainingCourse>(entity =>
            {
                ...

                entity.HasOne(d => d.Course)
                    .WithMany()
                    .HasForeignKey(d => d.CourseId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Person<>TrainingCourse_TrainingCourse");

                entity.HasOne(d => d.Person)
                    .WithMany()
                    .HasForeignKey(d => d.PersonId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Person<>TrainingCourse_Person");
            });

I have the same Problem. DBContext only hast one-to-many relationships even thought the database has many-to-many relations.

@ErikEJ
Copy link
Contributor

ErikEJ commented Nov 24, 2021

@moritzinio please open a new issue, and share your database schema.

@moritzinio
Copy link

@ErikEJ I will tomorrow thank you.

@sommmen
Copy link

sommmen commented Feb 22, 2022

@ajcvickers hiya this docs page https://docs.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#many-to-many

Points to this workitem which seems to be closed. Is this now fixed or not? perhaps the docs should be updated?

@smitpatel
Copy link
Contributor

Filed dotnet/EntityFramework.Docs#3760

@Scionn
Copy link

Scionn commented Jul 12, 2022

@ajcvickers hiya this docs page https://docs.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#many-to-many

Points to this workitem which seems to be closed. Is this now fixed or not? perhaps the docs should be updated?

I don't think its SOLVED, we have the same problem today with the latest version of EF core 6.0.6 (at the moment) scaffolding a Postgresql database with two m-2-m tables...

@ajcvickers
Copy link
Member Author

@Scionn Please open a new issue and post the schema and command that results in the issue so that we can investigate.

@RicardOlivo
Copy link

Maybe missing something here, but I'm baffled at how or why would Microsoft arbitrarily force a set of rules and decide which and when a table will be reverse engineered and when to skip it, without even a flag to supress it. Is there a way to full scaffold using net 6 without this nonsense, other than doing it by hand? thanks

@ErikEJ
Copy link
Contributor

ErikEJ commented Apr 20, 2023

@RicardOlivo Yes, just use EF Core Power Tools!

@tuyendam00
Copy link

I still want ef core gen class mapping many-many in net 6.0, how to do it
I want:
modelBuilder.Entity(
entity =>
{
entity.HasKey(e => new { e.GenerateProjectId, e.ProgrammingLanguageId });

            entity.ToTable("GenerateProjectProgrammingLanguageMapping", "MDM");

            entity.HasOne(d => d.GenerateProject)
                .WithMany(p => p.GenerateProjectProgrammingLanguageMapping)
                .HasForeignKey(d => d.GenerateProjectId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("FK_GenerateProjectProgrammingLanguageMapping_GenerateProject");

            entity.HasOne(d => d.ProgrammingLanguage)
                .WithMany(p => p.GenerateProjectProgrammingLanguageMapping)
                .HasForeignKey(d => d.ProgrammingLanguageId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("FK_GenerateProjectProgrammingLanguageMapping_ProgrammingLanguage");
        });

Ef gen:
entity.HasMany(d => d.ProgrammingLanguages)
.WithMany(p => p.GenerateProjects)
.UsingEntity<Dictionary<string, object>>(
"GenerateProjectProgrammingLanguageMapping",
l => l.HasOne().WithMany().HasForeignKey("ProgrammingLanguageId").OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_GenerateProjectProgrammingLanguageMapping_ProgrammingLanguage"),
r => r.HasOne().WithMany().HasForeignKey("GenerateProjectId").OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_GenerateProjectProgrammingLanguageMapping_GenerateProject"),
j =>
{
j.HasKey("GenerateProjectId", "ProgrammingLanguageId");

                        j.ToTable("GenerateProjectProgrammingLanguageMapping");
                    });

@RicardOlivo
Copy link

@tuyendam00 I did it using net core 3.1, then copy/pasted into 6. Sad I know.

@ErikEJ
Copy link
Contributor

ErikEJ commented Jun 8, 2023

@tuyendam00 @RicardOlivo Just use EF Core Power Tools, it has an option to generate the actual entities

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-many-to-many area-scaffolding closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. ef6-parity type-enhancement
Projects
None yet
Development

No branches or pull requests