0

I have a table and I want to insert entries with entity framework. By design, the table cannot have a meaningful primary key.

CREATE TABLE dbo.ChargeCarrier_Storage
(
    ID_ChargeCarrier INT NOT NULL,
    ID_Storage INT NULL,
    PickedUpOn DATETIME2(2) NULL,
    UnloadedOn DATETIME2(2) NULL,
    TransportedByDevice NVARCHAR(50) NOT NULL,

    CONSTRAINT FK_ChargeCarrier_Storage_to_Storage FOREIGN KEY (ID_Storage) REFERENCES Storage(ID) ON DELETE CASCADE, 
    CONSTRAINT FK_ChargeCarrier_Storage_to_ChargeCarrier FOREIGN KEY (ID_ChargeCarrier) REFERENCES ChargeCarrier(ID) ON DELETE CASCADE,
    CONSTRAINT CCS_OneNotNull CHECK (PickedUpOn IS NOT NULL OR UnloadedOn IS NOT NULL),
    CONSTRAINT CCS_OnForklift CHECK (ID_Storage IS NULL AND PickedUpOn IS NOT NULL OR ID_Storage IS NOT NULL)
)
GO

CREATE CLUSTERED INDEX IX_ChargeCarrier_Storage ON dbo.ChargeCarrier_Storage (ID_ChargeCarrier)
GO

CREATE UNIQUE INDEX IX_OnForklift ON dbo.ChargeCarrier_Storage (ID_ChargeCarrier, ID_Storage) WHERE ID_Storage IS NULL
GO

The table contains a track list of charge carriers and storage locations. The three non-id fields contain information about which forklift moved the charge carrier, and when. The initial entry for each charge carrier, created by the system, only contains an unload date and both IDs. When a forklift picks up something, a new entry should be created with only the three fields ID_ChargeCarrier, PickedUpOn and TransportedByDevice being set. As the forklift unloads the charge carrier, the entry should be updated with the unload date and the ID of the storage location where the piece was transported to.

ID_ChargeCarrier must always be filled. For each of those IDs, there can only be one single entry with ID_Storage set to NULL, as defined by IX_OnForklift. A charge carrier can appear on the same storage multiple times.

I could make the combination of ID_ChargeCarrier, ID_Storage and PickedUpOn the primary key, but that also doesn't work, because MS-SQL doesn't allow PKs with a nullable column.

As you can see, there is no other meaningful primary key. I strictly don't want to introduce an otherwise-superfluous ID column just to make EF happy.

How can I make the insert work anyways through Entity Framework?

3
  • EF requires PK, or in other words, cannot work with tables w/o PK. It's not just about insert, it won't let you include such entity model, hence you can't do anything with it. That's a known EF limitation, use something else for this table (ADO.NET for instance). Commented Sep 11, 2019 at 15:10
  • @IvanStoev Well, I could just write a simple insert statement for this case, but is that really a good solution? This is part of a legaycy product, so I can't simply change anything as I want. Commented Sep 11, 2019 at 15:40
  • 1
    Well, I can't say if it is good or bad, the point is that you have no option with EF, so anything else which works is good I guess :) Commented Sep 11, 2019 at 16:30

3 Answers 3

1

From the comments I see this is dealing with a legacy system.

What code would be considered the "owner" of this data, and how many other places (code, systems, reports, etc.) "touch" this data? Do you foresee needing to ever be querying against this data via EF?

If you just need to insert rows based on an event and don't care to ever query against this data via EF (at least for the foreseeable future) then I'd suggest merely inserting via a raw SQL statement and being done with it. New code for other areas may be starting to leverage EF, but "being consistent for consistency's sake" is never an argument I make. :)

The table design is poor. If this re-factoring needs to rely on this table, and the other touch-points are manageable then I would be arguing to re-design the table into something like:

ID_ChargeCarrier INT NOT NULL,
ID_Storage INT NULL,
EventTypeId INT,
EventOn DATETIME2(2) NOT NULL,
TransportedByDevice NVARCHAR(50) NOT NULL,

Where EventTypeId reflects a Pickup or DropOff and EventOn is the Date. This would accommodate a PK/unique constraint across ID_ChargeCarrier, EventTypeId, and EventOn. Heck, throw a PK column in, and re-factor TransportedByDevice to a FK to save space as I'm guessing this table will house a significant # of records. Porting existing data into a new structure shouldn't pose any issue other than processing time.

Or at a minimum keeping the same compatible structure, appending a proper PK into the table. For example you can use:

ID_ChargeCarrier INT NOT NULL,
ID_Storage INT NULL,
PickedUpOn DATETIME2(2) NULL,
UnloadedOn DATETIME2(2) NULL,
TransportedByDevice NVARCHAR(50) NOT NULL,
ID_ChargeCarrierStorage INT IDENTITY(1,1) NOT NULL

/w a PK constraint on the new identity column. This should be able to be appended without a table re-create. However, I expect that this table could be quite large so this would reflect a rather expensive operation that should be tested and scheduled accordingly.

EF needs a key defined to determine a unique row identifier. It doesn't even need to be declared as a PK in the database, though it is still restricted to using non-nullable fields. If these are records that you will be going to throughout the life of the system I would strongly recommend using a DB structure that accommodates a legal PK. I have had tables bound to entities that did not have PKs defined, but these were strictly transient staging tables where I loaded data from an external source like Excel, wired up some entities to the table to process the data and move relevant bits along to a permanent table.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank your very much for your comprehensive suggestions. I already had the idea to put everything transport-related into a separate table, because the design with all the nullables right now seems to be pretty complicated. The software came from a point, where one charge carrier could be loaded to one single storage place, and that was it. Now, that we can transport the thing from place to place, problems arose. Now, when I think about it, I could even see the forklift as a storage place and abstract the problem away that way. What do you think about that?
1

EF just needs an Entity Key. It doesn't have to map to a real database Primary Key.

And you should put an unique index on the mapped fields in the database (or risk poor performance and wierd behavior).

In SQL Server unique indexes can have nullable columns. And you can map non-nullable Entity Properties to nullable database columns.

Here's an example using that table definition:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;

namespace Ef6Test
{
    class ChargeCarrier_Storage
    {
        [Key()]
        [Column(Order =0)]
        public int ID_ChargeCarrier { get; set; }
        [Key()]
        [Column(Order = 1)]
        public int ID_Storage { get; set; }
        [Key()]
        [Column(Order = 2)]
        public DateTime PickedUpOn { get; set; }
        public DateTime UnloadedOn { get; set; }
        public string TransportedByDevice { get; set; }
    }
    class Db : DbContext
    {
        public Db(string constr) : base(constr) { }

        public DbSet<ChargeCarrier_Storage> ChargeCarrier_Storage { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

        }
    }

    class MyDbInitializer : IDatabaseInitializer<Db>
    {
        public void InitializeDatabase(Db context)
        {
            var sql = @"
 drop table if exists dbo.ChargeCarrier_Storage;

 CREATE TABLE dbo.ChargeCarrier_Storage
 (
     ID_ChargeCarrier INT NOT NULL,
     ID_Storage INT NULL,
     PickedUpOn DATETIME2(2) NULL,
     UnloadedOn DATETIME2(2) NULL,
     TransportedByDevice NVARCHAR(50) NOT NULL,

     --CONSTRAINT FK_ChargeCarrier_Storage_to_Storage FOREIGN KEY (ID_Storage) REFERENCES Storage(ID) ON DELETE CASCADE, 
     --CONSTRAINT FK_ChargeCarrier_Storage_to_ChargeCarrier FOREIGN KEY (ID_ChargeCarrier) REFERENCES ChargeCarrier(ID) ON DELETE CASCADE,
     CONSTRAINT CCS_OneNotNull CHECK (PickedUpOn IS NOT NULL OR UnloadedOn IS NOT NULL),
     CONSTRAINT CCS_OnForklift CHECK (ID_Storage IS NULL AND PickedUpOn IS NOT NULL OR ID_Storage IS NOT NULL)
 )


 CREATE CLUSTERED INDEX IX_ChargeCarrier_Storage ON dbo.ChargeCarrier_Storage (ID_ChargeCarrier)


 CREATE UNIQUE INDEX IX_OnForklift ON dbo.ChargeCarrier_Storage (ID_ChargeCarrier, ID_Storage) WHERE ID_Storage IS NULL

";
            context.Database.ExecuteSqlCommand(sql);
        }
    }


    class Program
    {

        static string constr = "server=.;database=ef6test;integrated security=true";


        static void Main(string[] args)
        {

            Database.SetInitializer<Db>( new MyDbInitializer());
            using (var db = new Db(constr))
            {
                var f = new ChargeCarrier_Storage();
                f.ID_ChargeCarrier = 2;
                f.ID_Storage = 2;
                f.PickedUpOn = DateTime.Now;
                f.TransportedByDevice = "SomeDevice";

                db.ChargeCarrier_Storage.Add(f);


                db.SaveChanges();
            }
            using (var db = new Db(constr))
            {
                var c = db.ChargeCarrier_Storage.First();

            }

            Console.WriteLine("Hit any key to exit");
            Console.ReadKey();

        }


    }
}

3 Comments

That's true for indexes, but not for primary keys. And EF requires a primary key to work unfortunately. As you can see, the table already has a clustered index.
A Primary Key is a database concept, and doesn't mean anything to EF. EF requires an Entity Key, or a set of Entity properties that identify an Entity instance. It does not need to map to a database Primary Key. It's a very similar concept, as an entity has one main, or "primary" key, and zero or more Alternate Keys. But not the same.
Thank you! This makes it clearer. Unfortunately we're not using the code first approach yet, but I see your point. I reworked the database.
1

Eventually I "solved" the problem by completely re-doing the table. I removed a lot of the clutter and added a primary key.

CREATE TABLE dbo.ChargeCarrier_Storage
(
    ID_ChargeCarrier INT NOT NULL,
    ID_Storage INT NOT NULL,
    ID_User INT NULL,
    StoredOn DATETIME2(2) NOT NULL DEFAULT GetDate(),

    CONSTRAINT PK_ChargeCarrier_Storage PRIMARY KEY (ID_ChargeCarrier, ID_Storage, StoredOn),
    CONSTRAINT FK_ChargeCarrier_Storage_to_Storage FOREIGN KEY (ID_Storage) REFERENCES Storage(ID), 
    CONSTRAINT FK_ChargeCarrier_Storage_to_ChargeCarrier FOREIGN KEY (ID_ChargeCarrier) REFERENCES ChargeCarrier(ID)
)
GO

I consider a forklift to be it's own "storage", so the loading and unloading is fully abstracted away now.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.