Feature Requests - "Denormalised ORM"

Ken Evans
2006-03-02
2013-05-28
  • Ken Evans

    Ken Evans - 2006-03-02

    I don't know how to respond directly in the "feature requests" part of this site so I'll have to put my comment here.

    Comments on feature requests by "bvanskvier".

    1: Your ORM diagram.
    This does not make sense.
    Addresses do not have credit cards.

    2: RE Your suggestion about putting the address data into the user table.
    The purpose of ORM is to help people to define a Universe of Discourse in an unambiguous way.
    Traditionally ORM has mapped to a 5NF logical model that preserves the property of "unambiguity".
    Your suggestion would destroy this property.
    I suggest you review chapter 1 of Terry's book "Information Modeling and Relational databases". You will also find it helpful to read Fabian Pascal's book " Practical Issues in Database Management"

    Hope this helps
    Ken

     
    • Bernd VanSkiver

      Bernd VanSkiver - 2006-03-02

      1. You are correct, Addresses do not have credit cards, but credit cards have a billing address. I just noticed that I did not put in predicate readings, which would have helped in this case.

      2. I am not trying to change ORM, I just would like a way to control a little more how objects get mapped to tables. I realize that this would denormalize the objects a little, but sometimes controlled denormalization is a good thing. Please review Chapter 12.7 of Terry's book. As in this example, in my implementation of the database I don't care about keeping addresses unique, other then their ID. My application is not an address management system, I don't need addresses to be its own object. But I don't want to have to add the address facts to every object that has an address. I would like to be able to add an Address to an object and then automatically have all the facts related to Address then added to the parent object.

      Maybe none of this makes sense to anyone else, I have deleted the request as noone seems to agree that it would be useful. I just know that in my pratical implementations of ORM and databases, this would be a useful feature to have in there.

       
    • Ken Evans

      Ken Evans - 2006-03-02

      OK,
      I have prepared a word document that shows an OR model and a derived logical model. I'm not sure how to do attachments in this forum so until I figure that out here are my main points.

      1: A credit card does not have a "billing address" It is the credit card holder who has the "billing address".

      2: An object-role model is a statement of the collective truth of a set of assertions about a universe of discourse. An object-role model is either true or false. It has nothing whatsoever to do with performance.

      2: Denormalization is a technique for solving performance problems.

      The concept of "performance" relates only to a physical instantiation of a logical schema.

      Hope this clarifies my position on this issue.
      By the way, I believe that it is very important to hammer this out in this public forum. Byu doing that I hope that together we can help others to grasp the fundamental differences between Conceptual schema (ORM), Logical schema (R-table structures) and physical implementations (DBMS product and implementation dependent)

      Regards
      Ken

       
    • Ken Evans

      Ken Evans - 2006-03-02

      Following my previous post, I have uploaded the file into the "Feature Requests" section since I could not find any other way to upload.

      Ken

       
    • Bernd VanSkiver

      Bernd VanSkiver - 2006-03-06

      1. I agree, credit cards don't actually have addresses, but you would then need a way to show which address a card uses. For example, I have an address that I live at, but I use a different address for one of my credit cards (my PO Box) for my billing address, while another card uses my home address.

      2. So when you build ORM models and want to denormalize them for performance reasons, do you not change the ORM model and just the implentation of it? I have thought about doing that, but it becomes a hassle to make future changes in the ORM model and then propogate them to the implementation.

       
    • Ken Evans

      Ken Evans - 2006-03-07

      In 1, you are making assertions about your circumstances (the real world)
      To record such "true facts" in ORM you might record the following facts:
      Person(ID)lives at Address(ID)
      Person(ID)has mail sent to Address(ID)
      Person(ID)works at Address(ID)

      It is the Person that plays roles with multiple Addresses not the credit card.

      You use ORM a a tool to record "true facts" about your "Universe of Discourse."
      A set of facts is just a set of facts. it can be neither normalised nor denormalised.

      If you change an OR model, the 5NF logical model that you then derive may be significantly different from the one that you had before the change. When you convert the new logical model to a new physical model then you may still have performance problems but they may well be quite different from those that you had in your original model. It all depends on the DBMS, hardware, application structure and so on.

      It may seem a "hassle" to do the round trip work but that is the only way that you can be sure that your physical database structure is "true" i.e. that is is an accurate reflection of your Universe of Discourse.

      In Terry's book (12.7) he says that denormalization is a "last resort" and should be done with extreme care.

      The point is that when you change the table structure, you are changing what the database means. So "just changing the OR model" is a synonym for "just changing the facts".

      In effect you are trying to change reality to solve a technical problem.
      Changing reality is not a practice that I recommend :-)

      Ken

       
      • Bernd VanSkiver

        Bernd VanSkiver - 2006-03-07

        1. Well, you still need to be able to record the fact that a certain address is related to a certain card. I guess you could do that with Person has mail sent to Address for CreditCard. Somehow that fact needs to be gathered.

        2. I had hoped that you had some magical way that you knew to do the round trip stuff, maybe one day it will be there :)

         
        • Ken Evans

          Ken Evans - 2006-03-09

          Regarding your point 2 "round trip stuff".

          The Microsoft tool can do this. The procedure is fully described in the book "Database Modeling with Microsoft Visio for Enterprise Architects" Morgan Kaufmann, 2003, ISBN 1-55860-919-9.

          Ken

           
        • Ken Evans

          Ken Evans - 2006-03-09

          "Person has mail sent to Address for CreditCard. Somehow that fact needs to be gathered. "

          Well, in my experience of opening bank accounts, the Bank always seems to be quite insistent on me filling out lots of forms that include "billing address".

          Maybe I don't quite understand what you are getting at here.

          Ken  

           
    • Jake

      Jake - 2006-03-07

      I know nothing about ORM nor do I know what schema you are talking about but
      concerning #1 I'd have to say that accounts (creditcard, bank account) do have addresses. While the person is the owner of all the addresses you must have a way to tie an address to an account. In reality an account could have multiple holders each having various addresses, but the account must know which of the addresses is the account address.

      Case in point, Wells Fargo allows you to have an address that propigates to all your accounts. They also allow Debit and Credit Card accounts to have seperate addresses as the primary address, not to mention having a secondary address.

      I know that I'm entering this conversation in the middle, but Ken I think that you need to explain why a CreditCard doesn't have an address and how you would determine the account address for a creditcard via the cardholder.

       
      • Ken Evans

        Ken Evans - 2006-03-09

        "but the account must know which of the addresses is the account address"

        Absolutely right.
        However "the problem" is that we tend to speak in metaphors and forget what the metaphors mean when we use them.

        A "bank account" is a metaphor for "a record of the transactions made by a legal or natural person"
         
        Substituting in your phrase we get:
        "but the
        [record of the transactions made by the person] must know which of the addresses is the
        [record of the transactions made by the person] address"

        So it is clear that an "account" is about the activities of a Person.
        The "linking" you mention is done by defining PK/FK relationships between tables such as "Person", "Transaction" and "Address".

        Ken  

         
    • Tyler

      Tyler - 2006-03-07

      Jake,

      I think it's times like this where "has" is over-used in ORM predicates.  I agree with Ken that a credit card doesn't have an address.  Not in the same way that I live at an address or that my computer is assigned an IP address.  However, I think it's fair to say that a credit card has its bill sent to a given address regardless of whether the person to whom the credit card is assigned actually lives there.  I had that problem pop up a few years ago, which is another story entirely...  

      In my (limited) experience, when you're making an ORM schema it's a balancing act to decide which perspective you're viewing the Universe of Discourse from.  Ken lives in a people-centric world where systems care

      Bernd,
      NORMA has what I'm told is a relatively simple way to add model extensions.  You can then record metadata about your object types and fact types, and then you could code an XSL transform based on that metadata to handle the denormalization for you.  The XSL is still an ugly process, but that way you can continue making your conceptual changes without having to hand-code all of your performance tweaks each time you regenerate the database schema.

      If you wanted to go that route, you could maybe add a "denormalize me" attribute to "Address".  Then write an XSL to modify the OIAL output.  From our chat the other day, you could insert your transform between the ORM to OIAL and the OIAL to DCIL transforms.  The down side is that you'd be dealing with reams of undocumented schemas.  Ugh.

      Currently, all the DDL is generated using XSLT.  I'd really like to see direct transforms based on ORM queries, but that's a ways down the road.  We need a good ORM2 meta-model (in ORM-- it exists in Microsoft's DSL tools framework at the moment) and a conceptual query language to make that work.  There's a conceptual query implementation planned to start this summer, so maybe check back then.

      Until then, I continue to sheepishly denormalize my databases by saying, "Person has primary- Phone", "Person has secondary- Phone", etc.

       
      • Ken Evans

        Ken Evans - 2006-03-09

        "... a credit card has its bill sent to a given address regardless of whether the person to whom the credit card is assigned actually lives there."

        Yesterday,I received a bill from the company from whom I buy water.

        Three instances of "name and adress (NAD)" appeared in different places on the bill.

        Instance 1 (on Page 1) headed : "Premises Supplied - Bill Amount"
        DATA: Someone elses name and someone elses address.

        Instance 2 (on Page 2) headed : "Premises Supplied - How your bill has been calculated"
        DATA: Someone elses name and someone elses address. (the same data as in instance 1)

        Instance 3: (at the bottom of Page 1)
        Was the "mailing address" that was printed in a position that ensured that the address showed through the window on the envelope.

        DATA: Someone elses name (same as instances 1 & 2) but MY ADDRESS!

        Note: The "someone else" is the person from whom I bought my house almost 4 years ago.

        The NAD's in instance 1 and instance 2 are correct.

        Now what is going on here?

        It is obvious that the data model in the water companies billing application is not true. It does not accurately reflect the "real" Universe of Discourse.

        I don't know why this is the case. However, this kind of thing can happen when a technician alters a database structure
        "to improve performance" without any grasp of the fact the he or she is changing the meaning of the axioms represented by the database.

        Ken
         

        1

         
    • Tyler

      Tyler - 2006-03-07

      Whoops!  I forgot to preview my last post.  My thought in the second paragraph was supposed to be continued....

      Ken lives in a people-centric world where systems care that a person can live at an address, work at another address, and have their bills sent to yet another address.  In Jake's world, Corporate America stores every possible place they can send the bill to to make sure they get paid.  I think they're both valid facts, but you just have to decide on a UoD to take your perspective from.

       
    • Ken Evans

      Ken Evans - 2006-03-09

      Good, we have a debate!
      It is true that everything depends on your UoD.
      However, I'm still not convinced about the logic of "credit cards having an address".
      Try subtracting objects and see what happens.

      1: If you subtract one of the addresses, the credit card lives on and so does the account, the other addresses and the person.

      2: If  subtract the credit card, the account still lives on and so do the addresses and the person.
      3: If you subtract the person (e.g. death),
      The physical addresses still exist but the credit card and account are meaningless.

      Why? Because a credit card is in reality just a number that links a physical or legal person and a bank account.

      I maintain my position: A credit card does not have an address. It is the person to whom the credit card is issued that has the address.

      Corporate America can send a bill to wherever it likes, but unless the bill is addressed to the right PERSON it probably won't be paid - and rightly so.

      Imagine you are the judge at the following court hearing:
      Bank's lawyer: "We sent the bill to the address on our files."
      Customer lawyer: "To whom was the bill addressed?"
      Bank's lawyer" Oh, we just sent it to the address - we didn't put a name on it."

      Who wins this case?

      Ken 

       
    • Jake

      Jake - 2006-03-09

      If I understand correctly, it would be best to create a fact that ties the entity (person, trust, corp), account and address together. Rather than trying to tie the account to an address.

      I think I need to see a model that allows the accounts to know which of the person addresses it is attached to. Is there somewhere online that offers examples?

       
      • Ken Evans

        Ken Evans - 2006-03-10

        Jake,
        I have created an example for you. However, my attempts to upload the example to the "feature requests" section of this forum have been met with the response "error" and no explanation.
        Any suggestions?
        Ken

         
      • Ken Evans

        Ken Evans - 2006-03-11

        Hi Jake,
        I have had no luck in uploading my very pretty file complete with ORM Schema, Logical Schema and Fact report. The best I can do is to show you the DDL. Here it is for SQL Server.
        Hope this helps.
        Ken

          /*    This SQL DDL script was generated by Microsoft Visual Studio. */

        /*    Driver Used : Microsoft Visual Studio - Microsoft SQL Server Driver.                    */
        /*    Document    : C:\Documents and Settings\Ken Evans\My Documents\ORM2_NORMA\SourceForgeExample1_LS.vsd. */
        /*    Time Created: 11 March 2006 18:37.                                                      */
        /*    Operation   : From Visio Generate Wizard.                                               */
        /*    Connected data source : No connection.                                                  */
        /*    Connected server      : No connection.                                                  */
        /*    Connected database    : Not applicable.                                                 */

        /* Create CreditCard database.                                                                */
        use master 

        go

        create database "CreditCard" 

        go

        use "CreditCard" 

        go

        /* Create new table "Account billed Person Address".                                          */
        /* "Account billed Person Address" : Account is billed to Person at Address                   */
        /*     "Account ID" : Account contains a record of transactions,                                 */
        /*     "Person id" : Address is a legal postal address.                                          */
        /*     "Address id" : Address is a legal postal address.                                         */ 
        create table "Account billed Person Address" (
            "Account ID" char(10) not null,
            "Person id" char(10) not null,
            "Address id" char(10) not null) 

        go

        alter table "Account billed Person Address"
            add constraint "Account billed Person Address_PK" primary key ("Account ID", "Address id")  

        go

        /* Create new table "CreditCard billed Person Address".                                       */
        /* "CreditCard billed Person Address" : CreditCard is billed to Person at Address             */
        /*     "CreditCard" : CreditCard provides the authorised user(Person) with Services.             */
        /*     "Person id" : Address is a legal postal address.                                          */
        /*     "Address id" : Address is a legal postal address.                                         */ 
        create table "CreditCard billed Person Address" (
            "CreditCard" char(10) not null,
            "Person id" char(10) not null,
            "Address id" char(10) not null) 

        go

        alter table "CreditCard billed Person Address"
            add constraint "CreditCard billed Person Address_PK" primary key ("CreditCard", "Address id")  

        go

        /* Create new table "Person Account Bank".                                                    */
        /* "Person Account Bank" : Person has Account with Bank                                       */
        /*     "Person id" : Address is a legal postal address.                                          */
        /*     "Account ID" : Account contains a record of transactions,                                 */
        /*     "Bank ID" : Role three (Bank) of fact: Person has Account with                            */ 
        create table "Person Account Bank" (
            "Person id" char(10) not null,
            "Account ID" char(10) not null,
            "Bank ID" char(10) not null) 

        go

        alter table "Person Account Bank"
            add constraint "Person Account Bank_PK" primary key ("Person id", "Bank ID")  

        go

        /* Create new table "Person".                                                                 */
        /* "Person" : Address is a legal postal address.                                              */
        /*     "Person id" : Address is a legal postal address.                                          */
        /*     "Lives Address id" : Address is a legal postal address.                                   */
        /*     "Name" : Person has Name                                                                  */ 
        create table "Person" (
            "Person id" char(10) not null,
            "Lives Address id" char(10) not null,
            "Name" char(10) not null) 

        go

        alter table "Person"
            add constraint "Person_PK" primary key ("Person id")  

        go

        /* Create new table "CreditCard".                                                             */
        /* "CreditCard" : CreditCard provides the authorised user(Person) with Services.              */
        /*     "CreditCard" : CreditCard provides the authorised user(Person) with Services.             */
        /*     "Person id" : Address is a legal postal address.                                          */
        /*     "Account ID" : Account contains a record of transactions,                                 */
        /*     "Issued Bank ID" : CreditCard is issued by Bank                                           */ 
        create table "CreditCard" (
            "CreditCard" char(10) not null,
            "Person id" char(10) not null,
            "Account ID" char(10) not null,
            "Issued Bank ID" char(10) not null) 

        go

        alter table "CreditCard"
            add constraint "CreditCard_PK" primary key ("CreditCard")  

        go

        /* Create new table "Bank".                                                                   */
        /* "Bank" : Table of Bank                                                                     */
        /*     "Bank ID" : Bank is identified by BankID                                                  */
        /*     "Address id" : Address is a legal postal address.                                         */ 
        create table "Bank" (
            "Bank ID" char(10) not null,
            "Address id" char(10) not null) 

        go

        alter table "Bank"
            add constraint "Bank_PK" primary key ("Bank ID")  

        go

        /* Create new table "Address".                                                                */
        /* "Address" : Address is a legal postal address.                                             */
        /*     "Address id" : Address is a legal postal address.                                         */
        /*     "City" : Address has City                                                                 */
        /*     "ZipCode" : Address has ZipCode                                                           */ 
        create table "Address" (
            "Address id" char(10) not null,
            "City" char(10) not null,
            "ZipCode" char(10) not null) 

        go

        alter table "Address"
            add constraint "Address_PK" primary key ("Address id")  

        go

        /* Add the remaining keys, constraints and indexes for the table "Account billed Person Address". */
        alter table "Account billed Person Address" add constraint "Account billed Person Address_UC1" unique (
            "Account ID",
            "Person id") 

        go

        /* Add the remaining keys, constraints and indexes for the table "CreditCard billed Person Address". */
        alter table "CreditCard billed Person Address" add constraint "CreditCard billed Person Address_UC1" unique (
            "CreditCard",
            "Person id") 

        go

        /* Add the remaining keys, constraints and indexes for the table "Person Account Bank".       */
        alter table "Person Account Bank" add constraint "Person Account Bank_UC1" unique (
            "Person id",
            "Account ID") 

        go

        alter table "Person Account Bank" add constraint "Person Account Bank_UC2" unique (
            "Account ID",
            "Bank ID") 

        go

        /* Add foreign key constraints to table "Account billed Person Address".                      */
        alter table "Account billed Person Address"
            add constraint "Person_Account billed Person Address_FK1" foreign key (
                "Person id")
             references "Person" (
                "Person id") 

        go

        alter table "Account billed Person Address"
            add constraint "Address_Account billed Person Address_FK1" foreign key (
                "Address id")
             references "Address" (
                "Address id") 

        go

        /* Add foreign key constraints to table "CreditCard billed Person Address".                   */
        alter table "CreditCard billed Person Address"
            add constraint "CreditCard_CreditCard billed Person Address_FK1" foreign key (
                "CreditCard")
             references "CreditCard" (
                "CreditCard") 

        go

        alter table "CreditCard billed Person Address"
            add constraint "Person_CreditCard billed Person Address_FK1" foreign key (
                "Person id")
             references "Person" (
                "Person id") 

        go

        alter table "CreditCard billed Person Address"
            add constraint "Address_CreditCard billed Person Address_FK1" foreign key (
                "Address id")
             references "Address" (
                "Address id") 

        go

        /* Add foreign key constraints to table "Person Account Bank".                                */
        alter table "Person Account Bank"
            add constraint "Person_Person Account Bank_FK1" foreign key (
                "Person id")
             references "Person" (
                "Person id") 

        go

        alter table "Person Account Bank"
            add constraint "Bank_Person Account Bank_FK1" foreign key (
                "Bank ID")
             references "Bank" (
                "Bank ID") 

        go

        /* Add foreign key constraints to table "Person".                                             */
        alter table "Person"
            add constraint "Address_Person_FK1" foreign key (
                "Lives Address id")
             references "Address" (
                "Address id") 

        go

        /* Add foreign key constraints to table "CreditCard".                                         */
        alter table "CreditCard"
            add constraint "Person_CreditCard_FK1" foreign key (
                "Person id")
             references "Person" (
                "Person id") 

        go

        alter table "CreditCard"
            add constraint "Bank_CreditCard_FK1" foreign key (
                "Issued Bank ID")
             references "Bank" (
                "Bank ID") 

        go

        /* Add foreign key constraints to table "Bank".                                               */
        alter table "Bank"
            add constraint "Address_Bank_FK1" foreign key (
                "Address id")
             references "Address" (
                "Address id") 

        go

        /* Create procedure/function Person_Account_Bank_equal1.                                      */
        /* /* The constraint:                                                                            */ */
        /* /* Implied equality constraint.                                                               */ */
        /* /* is enforced by the following DDL.                                                          */ */
        Create Procedure Person_Account_Bank_equal1 as
        /*   Microsoft Visual Studio generated procedure code. */
        if (
            not exists (select * from "Account billed Person Address" X where
                        not exists (select * from "Person Account Bank" Y
                                    where X."Account ID" = Y."Account ID")) and
            not exists (select * from "Person Account Bank" X where
                        not exists (select * from "Account billed Person Address" Y
                                    where Y."Account ID" = X."Account ID"))
        )
          return 1
        else
          return 2
        /* End Person_Account_Bank_equal1                                                             */

        go

        /* Create procedure/function Account_billed_Person_Address_subset2.                           */
        /* /* The constraint:                                                                            */ */
        /* /* Implied subset constraint.                                                                 */ */
        /* /* is enforced by the following DDL.                                                          */ */
        Create Procedure Account_billed_Person_Address_subset2 as
        /*   Microsoft Visual Studio generated procedure code. */
        if (
            not exists (select * from "CreditCard" X where
                        not exists (select * from "Account billed Person Address" Y
                                    where X."Account ID" = Y."Account ID"))
        )
          return 1
        else
          return 2
        /* End Account_billed_Person_Address_subset2                                                  */

        go

        /* This is the end of the Microsoft Visual Studio generated SQL DDL script.                   */

         
      • Sten Sundblad

        Sten Sundblad - 2006-03-17

        Jake,

        Here is an example of what you're looking for:

        Person(1) has Address(1)
        Person(1) has Address(2)
        Person(1) has Address(3)
        Person(1) has CreditCard(1)
        Person(1) has CreditCard(2)
        CreditCard(1) is charged to Address(1)
        CreditCard(2) is charged to Address(3)

        Conceptually, these examples indicate the following fact types and constraints:

        Person(ID) has Address(ID).
            It is possible that more than one Person has the same Address.
        Person(ID) has CreditCard(ID).
            Each CreditCard is of some Person.
            Each CreditCard is of at most one Person.
        CreditCard(ID) is charged to Address(ID).
            Each CreditCard is charged to some Address.
            Each CreditCard is charged to at most one Address.

        You also need an external constraint that guarantees that the Address a CreditCard is charged to is and Address that is of the Person that has the CreditCard.

        I hope this helps. Btw, I totally agree with everyone who claims that ORM is for conceptual analysis and modeling only. An ORM model - or part of it - should be turned into a logical data model only. The logical data model, then, is turned into a physical data model. It's the physical data model that should be de-normalized if this is what is needed. Let me also say that the different treatment in SOA of resource data and reference data might very well increase the mass of de-normalized data, but that has nothing to do with ORM.

         
    • James Hardiman

      James Hardiman - 2006-03-10

      Hi there, James Hardiman here; ORM aficionado for the last 14 or so years.

      I missed the original post here, so I can't comment directly, but I'm fascinated by the direction in which this discussion is going.  As I come at ORM from a somewhat different direction from Ken, I thought I'd throw in my 10c worth.

      For me, one of the key things that ORM gives us is the ability to model the UoD from the users' point of view, without having to worry about the technical implementation until much later.  Hence Terry calling this "conceptual modeling".

      It is usually the case IMHO (and experience), that domain users and experts have great experience of *working* in their domain, but don't have great experience of thinking very carefully about it.

      The sort of discussion that has been happening here is VITAL to have between the application designer and the user community.  However, I believe that it is not the responsibility of the designer to impose any particular view on the community, but to act as a sort of latter-day Socrates, and enter into a dialogue with them (probably using good old-fashioned Socratic dialectic) to explore their thinking about the domain in order to discover (and this will be a process of discovery for both sides) what the *best* way will be to represent that domain.

      So, I don't think that you will resolve this discussion until we introduce someone from a credit card company who understands the UoD from the users' point of view, and until we have entered into a Socratic dialogue with him or her to thoroughly examine the UoD.

      During such a process, we can represent in ORM whatever the current thinking about the domain may happen to be.

      When we arrive at a consensus that our model is "correct" (in the users' terms), and we can't see anything that we could dialectically challenge, then we can click the appropriate button in whatever tool we are using to see if the model is mathematically correct.

      At that point we generate the ER diagram, and then it's over to the DBA.  Whatever is done after that, whether it be de-normalisation for reasons of performance, or whatever, is down to the DBA's black arts ... and that's where I have to leave the conversation!

      Regards,

      Socrates2006

       
    • Corey Kaylor

      Corey Kaylor - 2006-03-10

      I agree with Ken so far regarding this discussion. I also feel that what James has said has an important part as well.

      Back to the original problem and the suggested solution, the desired results do not require the tool to handle something that could be modeled differently. The reference mode of id could be removed from address, an external uniqueness constraint could be added to the necessary part of an address, and the one to one could be changed to a many to one and the desired results would occur.

      Off Topic: I have put in a feature request to provide a different forum solution, due to the lack of features (i.e. images). Please feel free to provide input, and let the powers that be decide the best solution.

       

Log in to post a comment.

Get latest updates about Open Source Projects, Conferences and News.

Sign up for the SourceForge newsletter:





No, thanks