views:

215

answers:

5

I have an application where a multiple shops will share the data. There is an Options table that defines various program options. I have a varchar column that defines the value type, int, bool, Guid, string, etc. There are two tables that define the option values, one for system wide options and one for local options. The base Options table defines if the local option can be selected before the system wide option. The Shop and Global option tables are basically the same structure except the ShopOption table has a ShopId FK for the shop the record belongs to. Each of these tables store the option values as varchar although the string may represent an integer, Guid, bool, or may actually be a string. I need to show a form with a tab for the local option, a tab the global option, and a tab to specify if the shop can overide the global. What I am not doing right is to get an option entity and get the value as the type it should be.

For instance:
GetOption(SessionTimeout) should return an Option entity and the value should be an integer type.
GetOption(DefaultCustomer) should return an Option entity and the value should be a Guid type.

I am searching for an answer using design patterns, and think a factory pattern may be what I want, but I am just not getting it.

+2  A: 

Something I've seen quite often in similar situations is providing a typed default :

OptionEntity<T> GetOptions<T>(string OptionName, T defaultvalue);
Brann
why not just wrap it in a Generic method? OptionEntity<T> GetOptions<T>(string optionName, T defaultValue);
STW
definitely. I've updated my answer accordingly.
Brann
+4  A: 

What you're asking for is late-binding, ie the ability to assign the variable type at runtime instead of compile-time. The immediate answer is that C# doesn't currently support that, and when it is supported it still won't fully solve your issue.

Your best bet is to use generics, which will increase type-safety but still won't prevent stupid errors. Creating a method like:

public T GetOption<T>(string key)
{
    // Retrieve the option type and value
    // Check that the option type and return type (T) are compatible
    // cast the option value to T
    // return the value
}

will let you attempt to cast the database result to the return type of T, but it will generate an exception if the cast fails (ie: you try requesting a GUID option as an int).

STW
+9  A: 

The underlying issue is that you are suffering from the inner platform effect, whereby you are trying to make a database within a database by storing as varchar what should be distinct, typed columns.

You have given yourself the ability to add options at runtime. However, they won't mean anything if the application doesn't understand them, and you can't add that understanding at runtime. The set of options must be known at design-time, which means the schema can be known at design-time, which means you have no need to abstract the structure into varchar values.

Make a table with columns representing each of your options, and use normal ORM practices to declare a data type to which it maps.

The abstraction isn't actually buying you anything.

Edit in response to comment from OP:

To implement cascading settings, you could make an OptionSet table with a column per option. There would only be one row, representing the global set. For each option which can be overridden by a manager, add a nullable column to the Store table.

You can then have a method which asks a Store to coalesce the effective options:

public class Store
{
    public virtual bool? AllowSavePasswords { get; set; }

    public virtual OptionSet GetEffectiveOptions(OptionSet globalOptions)
    {
        return new OptionSet
        {
            AllowSavePasswords = this.AllowSavePasswords ?? globalOptions.AllowSavePasswords,
            LoginTimeout = globalOptions.LoginTimeout

            // Repeat pattern for all options
        }
    }
}

As you can see, this allows everything to remain strongly-typed, while addressing the issue of options which can't be overridden. It also expresses the intent of which options can be overridden by specifying all of them on the Store table (reflecting their scope) and making them nullable (reflecting their optional nature).

The nice part is that there are no new techniques to learn or "magic" to implement (unless you hadn't seen the ?? operator, which is the equivalent of T-SQL's COALESCE function).

Bryan Watts
I kind of disagree with this point of view. If I understand correctly you're suggesting creating an option table with 50 columns to accomodate all the possible types (or possibly 50 different tables?) I'm tempted to think the specialization is overkill for such a simple scenario as storing user defined options
Brann
I am suggesting using 50 typed columns to store 50 typed values. That is why we have columns. A high number of them doesn't change the usefulness of the column pattern, it just means there is more data to track. There is no difference between user-defined options as discussed here and any other set of values you store in a database.
Bryan Watts
@Bryan: In a scenario where a database schema can be easily changed with each new application release, you would be right. But in reality this is often not the case. If one wants to have a possibility to add new parameters to the option table with a new application release, but without the need of changing the database schema, it might be a good idea to have a generic option table.
Doc Brown
Well, this will result in a table containing 48 null values per line, and it will lead to a lot of repetition in your stored procedures. It's clearly good for type safety, but don't you think the readability and maintenability problems it creates outweight the benefits ?
Brann
If you mean creating one column per option (vs one column per type), this hurts the ability to add new options easily quite a lot (you need a schema change for each new option), not to mention the option logic (application wide settings first, the user settings, etc) being duplicated
Brann
I agree with Bryan. Your application knows what datatypes it needs at compile time. You'll never need the flexibility to change the timeout option to a GUID on the fly, though that change is more likely to happen by accident with this architecture and crash your app. This is one of those things that sounds nice in theory but is often pretty horrendous in practice--not to say that there is never a need for this architecture, but I think it is overkill for such a simple scenario.
dan
@Doc Brown, @Brann: With those arguments, why use tables and columns at all? You can implement Entity-Attribute-Value tables (a well-known anti-pattern) and that would remove the need to do schema updates in all future versions. My point is that there is nothing different about this set of values versus any other set for which you would make columns on a table. What happens if you want to add date-of-birth to the `User` table? It is the exact same situation as adding a new option, but you probably wouldn't argue that `User` should abstracted so we can avoid making schema changes.
Bryan Watts
@Bryan - Read a lot here so far. What I don't seem to understand is this: Are you saying that you want to have one column per datatype or, do you want to have one row per datatype?
Chris
@Bryan, it's just a matter of balance between being too generic (eg your extreme example with only one table) vs too specialized. Genericity has its advantage, and so does specialization. I think a good indicator is the need to duplicate code and application logic. I think your suggestion (ie one column per option) can easily lead to code duplication (because all options are handled the same way), which is definitely not the case for a user table.
Brann
@Bryan: I agree with totally with Brann. Have a look into Fowler's book I linked to in my answer if you want more suggestions where a generic solution makes sense (and where not). By the way, the OP said the database design is already generic, he was looking for an appropriate solution in his application code.
Doc Brown
I hope/think Bryan is indeed suggesting a column-per-datatype approach, in which case I can only agree
jeroenh
I am suggesting a column-per-option approach, where a row is one complete set of options. This is analogous to to a column-per-User-attribute approach, where a row is one complete user. Call the table and the entity `OptionSet`. When you need a DB timeout, do `GetCurrentOptionSet().DbTimeout`. I don't understand the *concrete benefit* of converting schema to data here. @Brann: Please elaborate on your assertion that all options are handled the same way. Each option is probably used in a very different place for a very different purpose. Just like `User` columns.
Bryan Watts
@Doc Brown: I have read Analysis Patterns. The quantity example you provide is not the same situation. In there, all the data was of the same type (number) and simply had an extra piece of information associated with it (unit). He still uses separate columns on the Person table. The OP is doing the equivalent of making a table which stores the height and weight attributes as rows, not columns, and stores the text representation of the quantities in the value column. Again, I don't see the concrete benefit of turning schema into data in this case.
Bryan Watts
@Doc Brown: Be sure to read my link on the inner platform effect, especially the part about the database world. I am not arguing that schema-as-data isn't a neat and intriguing way to approach data storage; I am merely saying that I don't see why a set of options with separate data types benefits from that approach while a set of user attributes with separate data types does not.
Bryan Watts
@Bryan : very interesting link, thanks for sharing it !
Brann
@Bryan: I see strong advantages to your solution, but I don't see how this can resolve the need for specifying that any single property may or may not be be allowed to be overridden at the store level. As the business owner I may prefer users don't save login passwords, but I want to give my individual store managers the ability to override that. Thus at the system level AllowSavePassword is false, but a store may set that to true. On the other hand I may require that all logins timeout after 30 minutes of inactivity and not allow individual stores to alter that.
Beaner
@Beaner: I updated my answer with a response.
Bryan Watts
@Bryan: you are right, it is not exactly the same situation, but I think it is to some degree comparable with it. I agree with you that this is a case that might be suffering from the inner platform effect, thanks for the link. If I had to make a decision which options should be modeled generic and which ones should be modeled explicitly as columns, I would need more information about the application at all. Nethertheless, the decision was already taken before, so lets hope it was taken with care.
Doc Brown
+3  A: 

Have a look at Martin Fowlers book Analysis Patterns. Fowler calls this "Quantity", look at the begin of chapter 3. That is essentially the same, just replace "Quantity" by "Option". Of course, thats just the database part of the problem. For the application part, I suggest using Yooder's solution.

Doc Brown
Interesting link that you provided.
Chris
I need to grab a copy of this.
Beaner
+2  A: 

There are two solutions, each having its merit and its drawbacks :

Option 1 : Genericity

a system wide option table, defined like this :

Create table tbGlobalOptions
(
OptionName Varchar(255) Identity,
OptionValue Varchar(255),
OptionType varchar(255)
isLocked bit --this indicated the value cannot be overridden by the user. 
)

And an user options table :

Create table tbUserOptions
(
OptionName varchar(255)
UserID bigint,
OptionValue varchar(255),
Active bit
)
-- extra fields for logging omitted 
-- keys omitted 

The code contains an enum matching the OptionName column, so parsing Options from the code is trivial.

Cons :

  • type safety can only be implemented using constraints or triggers (which is clearly harder to maintains than column types)
  • it's harder to use the stored options directly from the database (as the parsing logic lives in the application code)
  • retrieving all the options for a specific user is more costly (you cannot just select the user row)

Option 2 : Specialization (and strong typing)

A strongly typed option table containing one column per option

Create table tbOptions
(
UserId bigint, -- 0 for global defaults
Option1 int,
Option2 varchar(max)
Option3 int,
...
Option426 bit
)

Type safety is clearly a good thing, but here it has a huge cost :

  • adding a new option requires a schema change
  • the stored procedures used to update the table will contain a lot of duplicated code, as the logic (eg the isLocked mechanism, or some extra logging you might want to add) must be repeated over and over for each field. This is how you end up with stored procedures containing 1500 arguments.
  • This solution doesn't scale well, as a table cannot have an unlimited number of columns (see max values for SQL Server 2008 here for example).

If you have 5 options and if this number is likely to stay the same over time, the second solution has its merits.

If on the other hand you plan to end up with thousands of options, this sound like a no-brainer for me : go for genericity !


In your application code, your problem is quite easily solved using a generic method :

OptionEntity<T> GetOptions<T>(string OptionName, T defaultvalue);

Edit to answer Bryan's comment below :

And yes, if there are 10000 values to store, there will be 10000 columns. That is true for every single table you will ever write. There is nothing special about an option table. Nothing.

This all depends on the level of abstraction we chose. How would you store a chess board position for example? You can clearly use a 64 columns table (64 values -> 64 columns) or you can use a design with 4 columns only (game id, x, y, contents). Don't you think both can be adequate depending on the situation?

In this specific case, if options can be created on the fly, or if their numbers is expected to grow exponentially, those options are, to a certain extent, just another type of data. And you don't want to store data in your schema, do you?

Brann
Can you implement that method for the OP? It's not as simple as it seems, since you have to parse any number of data types, not just do a cast. Be sure use `CultureInfo.InvariantCulture` on all reads and writes. What would a query look like to, say, see the maximum session timeout across all users? Also, without the type in the option table, the database isn't self-describing. Do you think that has potential to confuse new developers? Finally, do you have an enumeration which specifies all available options?
Bryan Watts
Yes, I use an enum to store all the available options (using this enum, parsing the options *from the code* is trivial). Indeed, the main drawback of this approach is that the data isn't easily parsed/accessed/documented/etc directly from the database. As I said, there's a choice to do, and both solutions have their benefits. the number of options is probably a good criteria to make a choice (what if there are 10000 options? do you advocate making a table containing 10000 columns???)
Brann
I asked about the enumeration because you mentioned code duplication earlier. I didn't understand what you meant, since a data type which declares 10000 auto-properties is on the same order of magnitude as an enumeration with 10000 values. The ORM does the mapping, so I don't see how the strongly-typing the options makes more code. And yes, if there are 10000 values to store, there will be 10000 columns. That is true for every single table you will ever write. There is nothing special about an option table. Nothing. Except an urge to abstract.
Bryan Watts
Here is my question: abstracting the structure introduces a lot of issues, such as query complexity, decreased efficiency, decreased discoverability, the need to implement a general-purpose text conversion mechanism, a proprietary entity retrieval mechanism, and the need to explain and justify this approach to every subsequent developer. What is the justifying benefit? Not having to make schema changes? Is that an actual or perceived constraint?
Bryan Watts
As I wrote it : 1- this scales better (can you accommodate one million options with your system?). 2- this avoid code duplication in the update code (I assume you intended to use a single stored procedure containing code for every single column? same issue on scalability + code readability). Regarding the issues you mentionned, some can be discussed (eg efficiency if very few options are set)
Brann
A system with 1 million options at the same logical level has bigger design issues than expressability :-) But I would respond that the issue is the same for any entity. What is it about an option table that makes it different from any other table which could suffer from the same problem? Would you say it is code duplication to have a stored procedure which updates a `User` table with 10000 values? This is where I am struggling. Why *just* options? Why not every table? They all have the potential to grow to enormous size. Also, I let the ORM do updates (no sprocs). Less code by default!
Bryan Watts
@Bryan : I answered you in my post. the question is not "options" vs "every other table". In this specific case, if options can be created on the fly, or if their numbers is expected to grow exponentially, those options are, to a certain extent, just another type of data. And you don't want to store data in your schema, do you?
Brann
As stated in my answer, an option can't be created on the fly. The application has to know how to consume it, which means it is design-time knowledge. If it must known at design-time, why design it to be a run-time value? Also, each option has its own schema, including name and data type. An existing concept (column) is made to represent exactly that. You are re-inventing that concept (the inner platform effect). An option is just a value, like every other value stored in every other column in the database. So I ask: what is it about a timeout that makes it different from a username?
Bryan Watts
And, how could the option set grow exponentially? Wouldn't it just grow linearly until it hit 10000?
Bryan Watts
Again it depends on the context. See my chess board example at the end of this answer.
Brann
As with your previous Fowler/quantity example, the chess board isn't the same situation. With an x/y/piece schema, you are still storing the same kind of data in the piece column (a foreign key to a piece type, or null if empty). That is self-describing and strongly-typed. With the options, you are storing *different* kinds of data in the same column. You can't interpret that data without external knowledge, which is not the case for a chess square. You are building a database within a database, and bypassing an existing database implementation to do it. Don't reinvent the concept of a column.
Bryan Watts
@Brann: Some great discussion here. My bet guess is that there are ~150 options currently. We frequently get requests for more and we implement the requests that make sense as we are able. I liked Bryan's more type safe approach of a column per option which has the added benefit of needing only a single trip to the database to return all the values, but the idea of adding corresponding columns to the shop table does not appeal to me. While type safety is nice I believe a programmer has to take some responsibility to not do stupid things and your answer is closest to how I was leaning anyway...
Beaner
... I think I do sometimes suffer from Bryan's mention of the inner platform effect, and I think I will get Martin Fowler's Analysis Patterns book. In this instance I think generics, ane enumerated conversion types are the best answer for me.
Beaner