EntityFramework 6 - Handling User-defined Attributes - asp.net-mvc-5
Happy new year to all! I've just begun data modelling an ASP.NET MVC 5 app for a client who runs a Tool Hiring business. Part of the solution involves building an admin (backend) feature through which admin users can create/edit custom attributes or Tool Metadata that are attached to each tool from a particular tool group. I am working on the notion that at runtime the application shouldn't know what the Metadata Schema will be. So I started with this:
Yeah, I know ... another EAV nightmare! I know that if the data is correctly normalised, and relevant indexes are created, then it shouldn't be too bad. But honestly, I don't see any other choice. So for example:
Bosch Cordless Drill
Tool Group: Drills
Brand: Bosch (ListItem - prepopulated from MetaAttributeListOption table)
Type: Cordless (listItem - prepopulated from MetaAttributeListOption table)
Keyless Chuck: Yes (Boolean)
Voltage: 14.4Volts (Text)
...
Now these Attributes will serve 3 purposes:
Display on Frontend as "Specifications"
Used for filtering Tools on Frontend
(Potentially) Used in Reporting to determine "Popular Brands" (for example)
So I guess I'm stuck with an RDBMS (SQL Server) for this. I know that a popular approach towards this would be to use some NoSQL solution, but to be honest, I don't have much hands-on experience with it to use it in conjunction with MSSQL. I could combine the Values tables into one table where each datatype value is in its own column, but that will leave me with a lot of nulls to contend with.
So I'm left with the following questions if you could kindly help me out with:
Does this model work in terms of my requirement? I'm not sure I've designed the relationship of the MetaAttributeListOption table correctly.
Is there an alternative to this EAV approach?
Assuming that my model above (or derivative thereof) is my only option, how would I implement this with Entity Framework 6? For the ASP View Pages in the admin backend, I imagine I would need some sort of HTML Helper to determine the correct Editor to render and then populate accordingly.
I would greatly appreciate any help from the StackOverflow community on this. Please let me know if you need more information, and please do not close this if you deem it off-topic as I believe that my questions are programming related. Thank you!
EDIT:
I'm starting a bounty on this worth 200 of my own points...100 for assisting/advising me on my Questions 1 & 2, and 100 points for Question 3. Thank you
The question's model looks viable, and the relationships configured correctly, with the exception that redundant OptionLabels could be created if there are lots of duplicates. There are, however, some changes and de-normalizing compromises I would make. (See #3)
With your filtering and reporting requirements, and relative comfort with MSSQL I think using an RDBMS is your best bet
I've seen the approach shown below used in a few other developers' APIs, and it seems to be a good enough compromise that is less normalized, but makes the data model simpler and querying for values much more flexible
I've added MetaAttributeList to allow one list to apply to multiple MetaAttributes. In this model Booleans would be represented as a Yes/No ListOption.
The question's model would require that searches for values examine (one of) 3 tables, and that the applicable MetaAttribute always be known in advance
The question's model, by default with EF Code First, would have an issue with multiple CASCADE paths, that would require use of the FluentApi (not a huge deal, but can be inconvenient to keep track of)
This approach would (optionally?) require that enforcement of valid ListOption entries be handled in code rather than the database
Displaying different types of values would not require any additional work to render properly
The Admin Interface would need to check for a MetaAttribute.ListOption to determine whether to display a TextBox or ListItem (and possibly a checkbox if ListItemOptions are Yes/No)
You may want to add another table for ToolGroups that narrows the MetaAttributes presented to the user
Note: Since the EF method and language weren't specified, I used EF Code First and VB.Net. IMO Migrations and easier transition to EF7 are reason enough to use Code First. I like the readability of VB.Net a little better, but I'll happily change to C# if needed (or use this converter).
Imports System.ComponentModel.DataAnnotations
Namespace Models
'I didn't bother specifying string lengths with <StringLength(#)>
Public Class HireTool
Public Property Id As Integer
'... other properties
'Navigation Properties
Public Overridable Property HireToolMetaAttributes As ICollection(Of HireToolMetaAttribute)
End Class
Public Class MetaAttribute
Public Enum MetaAttributeTypeEnum
Text = 1
ListItem = 2
End Enum
Public Property Id As Integer
Public Property Code As String
Public Property Label As String
Public Property Type As MetaAttributeTypeEnum
Public Property Required As Boolean
Public Property Position As Integer
'Navigation Properties
Public Overridable Property List As MetaAttributeList
End Class
Public Class MetaAttributeList
Public Property ID As Integer
Public Property Name As String
'Navigation Properties
<Required>
Public Property ListOptions As ICollection(Of MetaAttributeListOption)
End Class
Public Class MetaAttributeListOption
Public Property Id As Integer
Public Property OptionLabel As String
End Class
Public Class HireToolMetaAttribute
Public Property Id As Integer
<Schema.Index> <StringLength(1000)>
Public Property Value As String
<Required>
Public Overridable Property HireTool As HireTool
<Required>
Public Overridable Property MetaAttribute As MetaAttribute
End Class
End Namespace
Edit: Here's the generated SQL:
CREATE TABLE [dbo].[MetaAttributeLists] (
[ID] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_dbo.MetaAttributeLists] PRIMARY KEY CLUSTERED ([ID] ASC)
);
CREATE TABLE [dbo].[HireTools] (
[Id] INT IDENTITY (1, 1) NOT NULL,
CONSTRAINT [PK_dbo.HireTools] PRIMARY KEY CLUSTERED ([Id] ASC)
);
CREATE TABLE [dbo].[MetaAttributeListOptions] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[OptionLabel] NVARCHAR (MAX) NULL,
[MetaAttributeList_ID] INT NULL,
CONSTRAINT [PK_dbo.MetaAttributeListOptions] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_dbo.MetaAttributeListOptions_dbo.MetaAttributeLists_MetaAttributeList_ID] FOREIGN KEY ([MetaAttributeList_ID]) REFERENCES [dbo].[MetaAttributeLists] ([ID])
);
CREATE TABLE [dbo].[MetaAttributes] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Code] NVARCHAR (MAX) NULL,
[Label] NVARCHAR (MAX) NULL,
[Type] INT NOT NULL,
[Required] BIT NOT NULL,
[Position] INT NOT NULL,
[List_ID] INT NULL,
CONSTRAINT [PK_dbo.MetaAttributes] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_dbo.MetaAttributes_dbo.MetaAttributeLists_List_ID] FOREIGN KEY ([List_ID]) REFERENCES [dbo].[MetaAttributeLists] ([ID])
);
CREATE TABLE [dbo].[HireToolMetaAttributes] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Value] NVARCHAR (1000) NULL,
[HireTool_Id] INT NOT NULL,
[MetaAttribute_Id] INT NOT NULL,
CONSTRAINT [PK_dbo.HireToolMetaAttributes] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_dbo.HireToolMetaAttributes_dbo.HireTools_HireTool_Id] FOREIGN KEY ([HireTool_Id]) REFERENCES [dbo].[HireTools] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_dbo.HireToolMetaAttributes_dbo.MetaAttributes_MetaAttribute_Id] FOREIGN KEY ([MetaAttribute_Id]) REFERENCES [dbo].[MetaAttributes] ([Id]) ON DELETE CASCADE
);
GO
CREATE NONCLUSTERED INDEX [IX_Value]
ON [dbo].[HireToolMetaAttributes]([Value] ASC);
GO
CREATE NONCLUSTERED INDEX [IX_HireTool_Id]
ON [dbo].[HireToolMetaAttributes]([HireTool_Id] ASC);
GO
CREATE NONCLUSTERED INDEX [IX_MetaAttribute_Id]
ON [dbo].[HireToolMetaAttributes]([MetaAttribute_Id] ASC);
GO
CREATE NONCLUSTERED INDEX [IX_MetaAttributeList_ID]
ON [dbo].[MetaAttributeListOptions]([MetaAttributeList_ID] ASC);
GO
CREATE NONCLUSTERED INDEX [IX_List_ID]
ON [dbo].[MetaAttributes]([List_ID] ASC);
Related
The Lookup drop down is not displaying code with description after selection
I have created a custom table with following fields and using it in grid for lookup. The following are the structure of the Table. CREATE TABLE KcLocationColor ( CompanyID int not null, Code nvarchar(30) collate database_default, [Description] nvarchar(512) collate database_default, CONSTRAINT PK_LocationColor PRIMARY KEY CLUSTERED (CompanyID,Code) ) I have declared the lookup using following statement [PXSelector(typeof(KcLocationColor.code), new Type[] { typeof(KcLocationColor.code), typeof(KcLocationColor.description) }, DescriptionField = typeof(KcLocationColor.description))] After selecting only code displays not with code and description I have used it with acumatica tables and it is working fine. I am not able to figure out the issue with custom table Regards, R.Muralidharan
Set respective Grid Column’s DisplayMode Property to Hint. At run time after selecting value from PXSelector it should appear as value - description
Replicating a database structure containing a hierarchy of type information. (Multi-level enums in C# ?)
In my database, I have a hierarchy of security type information. A security can have a type and a subtype. Types are unique integers. Subtypes are unique integers. I'd like to replicate this in C# somehow. Ideally, my code would look like this: int securityTypeA = Equity; int securitySubTypeA = Equity.ETF; int securityTypeB = Option; int securitySubTypeB = Option.OverTheCounter; Note that after these assignments, securitySubTypeA may have the same integer value as securityTypeB, but securityTypeA <> securityTypeB and securitySubTypeA <> securitySubTypeB. In my database, tblSecuritySubTypes has primary key secSubTypeId:int and foreign key secTypeId:int. My table tblSecurityTypes has primary key secTypeId:int. How can I do this in C#? If I had only one level of hierarchy, I'd simply use an enum.
I don't think you can do this without additional namespace elements; Equity could be in a static class as a const int but then you can't write Equity.ETF because Equity is an int. So you either have to create a static class called Equity (to be able to have ETF as a member; you can't use Equity as a value though) or have Equity as a value (but then you can't have nested members).
Saving record in Subsonic 3 using Active Record
I'm having trouble saving a record in Subsonic 3 using Active record. I've generated my objects using the DALs and tts and everything seems fine because the following test passes. I think that my connection string is correct or the generation wouldn't have succeeded. [Test] public void TestSavingAnEmail() { Email testEmail = new Email(); testEmail.EmailAddress = "newemail#test.com"; testEmail.Subscribed = true; testEmail.Save(); Assert.AreEqual(1, Email.All().Count()); } On the live side, the following code fails: protected void btEmailSubmit_Click(object sender, EventArgs e) { Email email = new Email(); email.EmailAddress = txtEmail.Text; email.Subscribed = chkSubscribe.Checked; email.Save(); } with a message of: Need to specify Values or a Select query to insert - can't go on! at the following line repo.Add(this,provider); line in my ActiveRecord.cs: public void Add(IDataProvider provider){ var key=KeyValue(); if(key==null){ var newKey=_repo.Add(this,provider); this.SetKeyValue(newKey); }else{ _repo.Add(this,provider); } SetIsNew(false); OnSaved(); } Am I doing something horribly wrong here? The save and add methods have parameterless overloads that I thought were safe to use. Do I need to pass a provider? I've googled around for this for a while and was unable to come up with anything specific to my situation. Thanks in advance for any kind of answer. The schema for the table is: USE [xxxx] GO /****** Object: Table [dbo].[Emails] Script Date: 03/11/2010 13:15:08 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO SET ANSI_PADDING ON GO CREATE TABLE [dbo].[Emails]( [Id] [int] IDENTITY(1,1) NOT NULL, [V_EmailAddress] [varchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, [B_Subscribed] [bit] NOT NULL, [DT_CreatedOn] [datetime] NOT NULL, [DT_ModifiedOn] [datetime] NOT NULL, CONSTRAINT [PK_Emails] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] GO SET ANSI_PADDING OFF There is only 1 warning during generation and that is Multiple template directives were found in the template. All but the first one will be ignored. Multiple parameters to the template directive should be specified within one template directive. Settings.ttinclude
The select error you're seeing is SubSonic trying to pull out the newly-created PK, and it can't. So, be sure you have a Primary Key defined for your table. Next - make sure it's set to Auto Increment :). If that doesn't do it - kick up SQL Profiler and see what's happening. Also - if you could put the schema of your table here so I could see it, that would be helpful (just edit your message).
This should generally work. A few possible points of failure come to my mind: Are you using a standard MSSQL provider (database)? Did you provide a connection string in the web.config (website) or app.config (class library project)? Did you set a primary key column in the database? Is your table using multiple primary key columns? Subsonic can't handle that. Use a single artificial ID column (uniqueidentifier or int) in that case. If the primary key value is an integer field: does it auto-increment the id values? Otherwise you'd have to set the primary key value on your email object before saving it.
SubSonic 3 / ActiveRecord - Easy way to compare two records?
With SubSonic 3 / ActiveRecord, is there an easy way to compare two records without having to compare each column by column. For example, I'd like a function that does something like this (without having to write a custom comparer for each table in my database): public partial class MyTable { public IList<SubSonic.Schema.IColumn> Compare(MyTable m) { IList<SubSonic.Schema.IColumn> columnsThatDontMatch = new...; if (this.Field1 != m.Field1) { columnsThatDontMatch.add(Field1_Column); } if (this.Field2 != m.Field2) { columnsThatDontMatch.add(Field2_Column); } ... return columnsThatDontMatch; } } In the end, what I really need is a function that tests for equality between two rows, excluding the primary key columns. The pseudo-code above is a more general form of this. I believe that once I get the columns that don't match, I'll be able to check if any of the columns are primary key fields. I've looked through Columns property without finding anything that I can use. Ideally, the solution would be something I can toss in the t4 file and generate for all my tables in the database.
The best way, if using SQL Server as your backend as this can be auto populated, is to create a derived column that has a definition that uses CHECKSUM to hash the values of "selected" columns to form a uniqueness outside of the primary key. EDIT: if you are not using SQL Server then this hashing will need to be done in code as you save, edit the row.
best practices with code or lookup tables
[UPDATE] Chosen approach is below, as a response to this question Hi, I' ve been looking around in this subject but I can't really find what I'm looking for... With Code tables I mean: stuff like 'maritial status', gender, specific legal or social states... More specifically, these types have only set properties and the items are not about to change soon (but could). Properties being an Id, a name and a description. I'm wondering how to handle these best in the following technologies: in the database (multiple tables, one table with different code-keys...?) creating the classes (probably something like inheriting ICode with ICode.Name and ICode.Description) creating the view/presenter for this: there should be a screen containing all of them, so a list of the types (gender, maritial status ...), and then a list of values for that type with a name & description for each item in the value-list. These are things that appear in every single project, so there must be some best practice on how to handle these... For the record, I'm not really fond of using enums for these situations... Any arguments on using them here are welcome too. [FOLLOW UP] Ok, I've gotten a nice answer by CodeToGlory and Ahsteele. Let's refine this question. Say we're not talking about gender or maritial status, wich values will definately not change, but about "stuff" that have a Name and a Description, but nothing more. For example: Social statuses, Legal statuses. UI: I want only one screen for this. Listbox with possibe NameAndDescription Types (I'll just call them that), listbox with possible values for the selected NameAndDescription Type, and then a Name and Description field for the selected NameAndDescription Type Item. How could this be handled in View & Presenters? I find the difficulty here that the NameAndDescription Types would then need to be extracted from the Class Name? DB: What are pro/cons for multiple vs single lookup tables?
Using database driven code tables can very useful. You can do things like define the life of the data (using begin and end dates), add data to the table in real time so you don't have to deploy code, and you can allow users (with the right privileges of course) add data through admin screens. I would recommend always using an autonumber primary key rather than the code or description. This allows for you to use multiple codes (of the same name but different descriptions) over different periods of time. Plus most DBAs (in my experience) rather use the autonumber over text based primary keys. I would use a single table per coded list. You can put multiple codes all into one table that don't relate (using a matrix of sorts) but that gets messy and I have only found a couple situations where it was even useful.
Couple of things here: Use Enumerations that are explicitly clear and will not change. For example, MaritalStatus, Gender etc. Use lookup tables for items that are not fixed as above and may change, increase/decrease over time. It is very typical to have lookup tables in the database. Define a key/value object in your business tier that can work with your view/presentation.
I have decided to go with this approach: CodeKeyManager mgr = new CodeKeyManager(); CodeKey maritalStatuses = mgr.ReadByCodeName(Code.MaritalStatus); Where: CodeKeyManager can retrieve CodeKeys from DB (CodeKey=MaritalStatus) Code is a class filled with constants, returning strings so Code.MaritalStatus = "maritalStatus". These constants map to to the CodeKey table > CodeKeyName In the database, I have 2 tables: CodeKey with Id, CodeKeyName CodeValue with CodeKeyId, ValueName, ValueDescription DB: alt text http://lh3.ggpht.com/_cNmigBr3EkA/SeZnmHcgHZI/AAAAAAAAAFU/2OTzmtMNqFw/codetables_1.JPG Class Code: public class Code { public const string Gender = "gender"; public const string MaritalStatus = "maritalStatus"; } Class CodeKey: public class CodeKey { public Guid Id { get; set; } public string CodeName { get; set; } public IList<CodeValue> CodeValues { get; set; } } Class CodeValue: public class CodeValue { public Guid Id { get; set; } public CodeKey Code { get; set; } public string Name { get; set; } public string Description { get; set; } } I find by far the easiest and most efficent way: All code-data can be displayed in a identical manner (in the same view/presenter) I don't need to create tables and classes for every code table that's to come But I can still get them out of the database easily and use them easily with the CodeKey constants... NHibernate can handle this easily too The only thing I'm still considering is throwing out the GUID Id's and using string (nchar) codes for usability in the business logic. Thanks for the answers! If there are any remarks on this approach, please do!
I lean towards using a table representation for this type of data. Ultimately if you have a need to capture the data you'll have a need to store it. For reporting purposes it is better to have a place you can draw that data from via a key. For normalization purposes I find single purpose lookup tables to be easier than a multi-purpose lookup tables. That said enumerations work pretty well for things that will not change like gender etc.
Why does everyone want to complicate code tables? Yes there are lots of them, but they are simple, so keep them that way. Just treat them like ever other object. Thy are part of the domain, so model them as part of the domain, nothing special. If you don't when they inevitibly need more attributes or functionality, you will have to undo all your code that currently uses it and rework it. One table per of course (for referential integrity and so that they are available for reporting). For the classes, again one per of course because if I write a method to recieve a "Gender" object, I don't want to be able to accidentally pass it a "MarritalStatus"! Let the compile help you weed out runtime error, that's why its there. Each class can simply inherit or contain a CodeTable class or whatever but that's simply an implementation helper. For the UI, if it does in fact use the inherited CodeTable, I suppose you could use that to help you out and just maintain it in one UI. As a rule, don't mess up the database model, don't mess up the business model, but it you wnt to screw around a bit in the UI model, that's not so bad.
I'd like to consider simplifying this approach even more. Instead of 3 tables defining codes (Code, CodeKey and CodeValue) how about just one table which contains both the code types and the code values? After all the code types are just another list of codes. Perhaps a table definition like this: CREATE TABLE [dbo].[Code]( [CodeType] [int] NOT NULL, [Code] [int] NOT NULL, [CodeDescription] [nvarchar](40) NOT NULL, [CodeAbreviation] [nvarchar](10) NULL, [DateEffective] [datetime] NULL, [DateExpired] [datetime] NULL, CONSTRAINT [PK_Code] PRIMARY KEY CLUSTERED ( [CodeType] ASC, [Code] ASC ) GO There could be a root record with CodeType=0, Code=0 which represents the type for CodeType. All of the CodeType records will have a CodeType=0 and a Code>=1. Here is some sample data that might help clarify things: SELECT CodeType, Code, Description FROM Code Results: CodeType Code Description -------- ---- ----------- 0 0 Type 0 1 Gender 0 2 Hair Color 1 1 Male 1 2 Female 2 1 Blonde 2 2 Brunette 2 3 Redhead A check constraint could be added to the Code table to ensure that a valid CodeType is entered into the table: ALTER TABLE [dbo].[Code] WITH CHECK ADD CONSTRAINT [CK_Code_CodeType] CHECK (([dbo].[IsValidCodeType]([CodeType])=(1))) GO The function IsValidCodeType could be defined like this: CREATE FUNCTION [dbo].[IsValidCodeType] ( #Code INT ) RETURNS BIT AS BEGIN DECLARE #Result BIT IF EXISTS(SELECT * FROM dbo.Code WHERE CodeType = 0 AND Code = #Code) SET #Result = 1 ELSE SET #Result = 0 RETURN #Result END GO One issue that has been raised is how to ensure that a table with a code column has a proper value for that code type. This too could be enforced by a check constraint using a function. Here is a Person table which has a gender column. It could be a best practice to name all code columns with the description of the code type (Gender in this example) followed by the word Code: CREATE TABLE [dbo].[Person]( [PersonID] [int] IDENTITY(1,1) NOT NULL, [LastName] [nvarchar](40) NULL, [FirstName] [nvarchar](40) NULL, [GenderCode] [int] NULL, CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED ([PersonID] ASC) GO ALTER TABLE [dbo].[Person] WITH CHECK ADD CONSTRAINT [CK_Person_GenderCode] CHECK (([dbo].[IsValidCode]('Gender',[Gendercode])=(1))) GO IsValidCode could be defined this way: CREATE FUNCTION [dbo].[IsValidCode] ( #CodeTypeDescription NVARCHAR(40), #Code INT ) RETURNS BIT AS BEGIN DECLARE #CodeType INT DECLARE #Result BIT SELECT #CodeType = Code FROM dbo.Code WHERE CodeType = 0 AND CodeDescription = #CodeTypeDescription IF (#CodeType IS NULL) BEGIN SET #Result = 0 END ELSE BEGiN IF EXISTS(SELECT * FROM dbo.Code WHERE CodeType = #CodeType AND Code = #Code) SET #Result = 1 ELSE SET #Result = 0 END RETURN #Result END GO Another function could be created to provide the code description when querying a table that has a code column. Here is an example of querying the Person table: SELECT PersonID, LastName, FirstName, GetCodeDescription('Gender',GenderCode) AS Gender FROM Person This was all conceived from the perspective of preventing the proliferation of lookup tables in the database and providing one lookup table. I have no idea whether this design would perform well in practice.