I am about to implement a database for simple ecommerce platform. I want to implement the following:
Each product belongs to one product category;
Each product category has its own attributes;
Each product has one value for each attribute of this products type.
What relations should I use to store this kind of information?
Here is the logical model -- the way I understood it; you should be able to tweak it.
From this you can derive the physical model and the SQL code. The word KEY here means UNIQUE NOT NULL and you may use them for primary keys. Should you choose to introduce integers as primary keys, make sure you keep these UNIQUE.
Note that everything should be NOT NULL, once you get to the SQL.
Category named (CAT) exists.
Category {CAT}
KEY {CAT}
Attribute named (ATR) exists.
Attribute {ATR}
KEY {ATR}
Category (CAT) has attribute (ATR).
Each category has more than one attribute, it is possible for the same attribute to belong to more than one category.
CategoryAttribute {CAT, ATR}
KEY {CAT, ATR}
Product named (PRD) belongs to category (CAT).
Each product belongs to exactly one category, each category may have more than one product.
ProductCategory {PRD, CAT}
KEY {PRD}
KEY {PRD, CAT} -- seems redundant here, but is
-- needed for the FK from the next table
FOREIGN KEY {CAT} REFERENCES Category {CAT}
Product (PRD) from category (CAT) has attribute (ATR) that belongs to that category.
For each attribute that belongs to a category, that attribute may belong to more than one product from that category.
ProductCategoryAttribute {PRD, CAT, ATR}
KEY {PRD, CAT, ATR}
FOREIGN KEY {PRD, CAT} REFERENCES ProductCategory {PRD, CAT}
FOREIGN KEY {CAT, ATR} REFERENCES CategoryAttribute {CAT, ATR}
I don't know what database platform you are using, but for small numbers of products, and for queries that do not depend on the value of the per-category attributes, I'd use the following strategy:
CREATE TABLE "Category" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT
);
CREATE TABLE "Product" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"categoryId" INTEGER NOT NULL REFERENCES "Category" ("id"),
"attributes" TEXT NOT NULL
);
In this example, the categories are used mainly to enforce referential integrity and to provide a list of categories for navigation.
The attributes are stored inside the attributes column as JSON (most modern databases tend to support this natively).
If there are any attributes common to all types of products, we'd create specific columns in Product. For example, you could add creationDate, deletionDate, price, or whatnot.
This allows you to perform the typical Select * From Product Where id = #Id to get a specific product and Select * From Product Where categoryId = #CategoryId to get all products in a category.
A creationDate could be useful to sort the products by creation date and take the top N, if necessary, when filtering by category. However with small quantities like thousands of products you might as well get all products by category and do this in code.
Regarding the code aspect, products like Dapper have specific extensions helping you deal with these discriminated unions, but writing code to support it is fairly easy. Here's an how. I'll write pseudo-C#, but I'm sure you can adapt.
We have an abstract class taking care of the Product table rows
public abstract class ProductBase
{
// only the fields in the Product table here
public int CategoryId { get; set; }
protected string Attributes { get; set; }
// serialize extra fields to JSON in Attributes
protected abstract void Prepare();
// load the common fields from a data row
protected static ProductBase(DataRow dr)
{
CategoryId = int.Parse(dr["categoryId"]);
Attributes = dr["attributes"] as string;
}
// save to DB
public void Save()
{
Prepare();
// save to SQL
}
}
We also have specific classes per category which have the extra attributes and handle serialization and deserialization.
public class FooProduct: ProductBase
{
public string Color { get; set; }
protected override void Prepare()
{
Attributes = Json.Serialize(new { Color });
}
public FooProduct(DataRow dr): base(dr)
{
// we can only create foo products if the category is foo
if (CategoryId != 23) throw new InvalidOperationException();
var attr = Json.Deserialize(Attributes);
Color = attr.Color;
}
}
This idea works great while you don't need to get the "foo" products by Color. If you can afford to get all "foo" products and filter in code, great. If your database understands JSON and lets you query inside the Attributes field, good it will get slow with large numbers unless the server allows indexes to reference JSON-serialized values.
If all else fails, you'll need to create an index table which contains the color values and the ids of the products which have that color. This is relatively painful and you don't want to do it unless you need it (and you don't right now).
Related
i have 3 tables with one-to-one relationship. The phone table has one to one relationship with Model table and Model table has a one to one relationship with Manufacturer table.
phone_table
id
imei
image
model_id
model_table
id
name
image
manufracturer_id
manufracturer_table
id
name
logo
how to get a result like this :-
App\Phone{
imei : "356554512522148",
model : "Galaxy S-10",
manufracturer : "Samsung",
}
I would never throw it into the same array / object, i would firstly do that on transformation. If you use default Laravel transformation you can use getters for it. Simple example on how to access these fields into the same context would be.
$phone = Phone::with('model.manufactor')->find(1);
With secures the queries are optimal for accessing it. How to get data into same layer.
[
'imei' => $phone->imei,
'model' => $phone->model->name,
'manufactor' => $phone->model->manufactor->name,
]
For this to work, you need relations in your model too.
Phone.php
public function model()
{
return $this->belongsTo(Model::class);
}
Model.php
public function manufactor()
{
return $this->belongsTo(Manufactor::class);
}
Just join them:
\App\Phone::leftjoin('model_table AS mo', 'mo.id', '=','phone_table.model_id')
->leftjoin('manufracturer_table AS ma', 'ma.id', '='. 'mo.manufracturer_id')
->selectRaw('phone_table.imei, mo.name AS model, ma.name AS manufracturer')
->first()
And sometimes you need to think about why you want to split table to one-to-one relationship.
Is there a table not usually be used, or one of them need to be connected by another tables. is this just for saving space or reduce IO cost.
If there are not any other reason and you always need to get these tables' information, maybe you can merge to one table.
I have tried to make a logical data model, but I am not totally sure if it is modeled right. It is a very cut-down and basic model, but overall I want to know if it is modeled the way is should be.
Furthermore, how do I convert this into a class model in object oriented programming?
I guess I need:
Class Customer: int id, string name
Class Order: int id, string date, Customer object
Class Item: int id, string itemName, string item Desc
Class OrderItem: ?
For your data model, you don't need the relationship line between Orders and Items. You're using the junction table Order_Items to represent that many to many relationship.
As for the class models, you won't need a class to model the junction table. You can simply model it with a collection of Item in your Order class. The relationship between the Order and Item class is a composition relationship. You can think of it as: An Order has-a Item or an Order has-s collection of Item.
Here is how you can model the Order class in java.
public class Order {
private int id;
private Date date;
private Customer customer;
private List<Item> items; // you could use other collection types as well.
...
}
Edit:
Also for your many side of the relationships, you may consider using "one through many" line (crows foot with a line)" as opposed to "zero through many" (crows foot with circle). A order generally has atleast 1 item and atleast 1 customer. An order isn't an order without a customer or items.
I have a database where I want to store multiple items, every item would be unique, but they can have alternatives from other makers. So items would have a potential M:M relationship with each other.
For example, item A could have 3 alternatives. If I add item B, the alternative, the link for the alternative item should also be reversed, so that when I search item B, I would find item A as an alternative. When I add item C, it should be added as an alternative for both item A and B, and C should have as it's alternatives item A and B.
How's the best/smartest way to make this relationship between a class of its own kind?
The way I'm thinking is making a table in between called alternatives with a unique ID for each new item, if an alternative is added, then it's linked to that parent ID (alternative ID) and thus no new ID would be made in the alternative table. If it's established only later that this is an alternative, then remove the alternative ID of one of the two and add the item to the other alternative ID.
public partial class Item : Entity
{
public Item()
{
Id = GuidComb.GenerateComb();
}
public Guid Id { get; set; }
public string ItemName { get; set; }
public string MakerRef { get; set; }
public string Description { get; set; }
public virtual Maker Maker { get; set; }
public virtual IList<Offer> Offers { get; set; }
//stuck here on the smart way to make the relationship with itself?
}
Any advice would be welcome!
Thanks in advance!
The suggestion I'm thinking about might be similar to what you proposed, but I'll try to formulate it in database terms.
As far as I understand your requirements, your alternatives relationship will be completely transitive. This means your item set is partitioned in equivalence classes of subsets containing mutually alternative items. (If an item has no alternative yet, the subset consists of this item alone.)
If that's true, then the most elegant and redundancy free way to represent this is to choose one of the items of such as subset as a representative of the entire subset. This is reflected by the following table design:
item(id, equivalence_id, other attributes, ...)
where equivalence_id is a foreign key to the representative. Each item gets born with an equivalence id of null. If it is made equivalent to another item,
if the item already present has an equivalence id of null, assign the id of this representative to the equivalence it of both items,
if the item already present has a non-null equivalence id, assign this to the equivalence id of the new item.
Note that this works, in case there are many items in the same equivalence class, no matter which of this items are used to link the new one.
Example:
id equivalence_id name
1 1 abc
2 def
3 1 ghi
4 4 jkl
5 4 mno
This means abc and ghi are equivalent, as well as jkl and mno, but def isn't yet equivalent to anything. Now if pqr comes along and should become equivalent to abc, it would get equivalence id 1. The effect is the same as making it equivalent to ghi.
To find all items equivalent to a specific one, query
select *
from item
where equivalence_id = :my_equivalence_id
If some information pertaining to the equivalence class as a whole should be stored, a separate table for the equivalence classes only should be created.
The structure of concerning tables is as follows (MySQL):
//Table Name : team
tid PK
team_name (varchar)
//Table Name : fixture
fid PK
home_team_id FK |_ both referenced to 'tid' from 'team' table
away_team_id FK |
My aim is to retrieve the team names. Considering this structure, I think I'll have to retrieve home_team_id and away_team_id and then do something like
Fixture::where('tid','=',$home_team_id)->get();
My question is, is this the correct way to accomplish what I aim to do?
and
should this be done from the controller? (if so, then I'll have to do two queries from same function)
First, rather than having your primary keys be tid and fid, just keep them both as id. This is not only best practice, but will allow you to more easily use Laravel's Eloquent ORM as it by default assumes your primary key column is named id.
Second thing, make sure your table names are in plural form. Although this is not necessary, the example I'm about to give is using Laravel defaults, and Laravel assumes they are in plural form.
Anyway, once you've 'Laravelized' your database, you can use an Eloquent model to setup awesome relationships with very minimal work. Here's what I think you'd want to do.
app/models/Team.php
class Team extends Eloquent {
// Yes, this can be empty. It just needs to be declared.
}
app/models/Fixture.php
class Fixture extends Eloquent {
public function homeTeam()
{
return $this->belongsTo('Team', 'home_team_id');
}
public function awayTeam()
{
return $this->belongsTo('Team', 'away_team_id');
}
}
Above, we created a simple model Team which Laravel will automatically look for in the teams database table.
Second, we created model Fixture which again, Laravel will use the fixtures table for. In this model, we specified two relationships. The belongsTo relationship takes two parameters, what model it is related to, in both cases here they are teams, and what the column name is.
Laravel will automatically take the value in away_team_id and search it against the id column in your teams table.
With just this minimal amount of code, you can then do things like this.
$fixture = Fixture::find(1); // Retrieves the fixture with and id of 1.
$awayTeam = $fixture->awayTeam()->first(); // var_dump this to see what you get.
$homeTeam = $fixutre->homeTeam()->first();
Then you can proceed as normal and access the column names for the tables. So say you have a 'name' column in the teams table. You can echo out the the home team name from the fixture like so.
$fixture = Fixture::find(1); // Get the fixture.
echo $fixture->homeTeam->name;
It's nearly 2AM, so there might be an error or two above, but it should work.
Make sure you check the docs for Eloquent, especially the bits relating to relationships. Remember to name your columns and tables in the way Laravel wants you to. If you don't, there are ways to specify your custom names.
If you want to get even more fancy, you can define the inverse relationship like this on your Team model.
app/models/Team.php
class Team extends Eloquent {
public function fixturesAtHome()
{
return $this->hasMany('Fixture', 'home_team_id');
}
public function fixturesAway()
{
return $this->hasMany('Fixture', 'away_team_id');
}
}
Then to get all of a particular team's home fixtures...
$team = Team::find(1); // Retreive team with id of 1;
$homeFixtures = $team->fixturesAtHome();
If i have SQL Server tables like this:
Location
----------
LocationId int PK
Field1 int
Field2 int
etc
Score
------------------------------------
LocationId int PK, FK
IsSingleLevel bit PK (discriminator)
Field1 int
Field2 int
etc
Technically this is mapped as a Location 1..0..* Score, but with the PK of LocationId/IsSingleLevel, it's a Location 1..0..2.
When i drag this on the EDMX, set everything up as expected (abstract entity, remove discriminator from base entity, etc).
EF give this error three times (one for base entity, and one for the two derived entities):
Error 6 Error 3025: Problem in mapping fragments starting at line 2146:Must specify mapping for all key properties (LocationScore.LocationId, LocationScore.IsSingleLevelScore) of table LocationScore.
I followed the example here.
The error occurs because i have the discriminator as part of the PK in the database, and discriminators are not mapped on the model, so i get the error.
I can't have LocationId as the only field in the PK, because then a location could only have 1 score, i need it to have two scores (one single level, one overall).
The end result is i want to be able to do this:
Locations.Include("LocationOverallScore").Single();
Locations.Include("LocationSingleLevelScore").Single();
Where LocationOverallScore and LocationSingleLevelScore are derived entities from the LocationScore base (abstract entity).
Is TPH not the right design here? Is my database design wrong? The aim is i don't want to have 2 physical tables for the different scores - as the table is huge, and i don't want to repeat the columns.
There's two possible workarounds i can think of:
1 - Make a view (LocationScore), which UNION's the two tables (so it would return 2 rows per LocationId) - but i still don't think i can "TPH" this. I don't want to manually perform the JOIN, i want to eager load.
2 - Add an IDENTITY column to Score, and this can be the PK.
Can you guys think of another solution?
I found a solution. I'm not 100% happy with it, but it does satisfy my requirement of not having two tables.
In the database side:
LocationScore:
LocationScoreId: IDENTITY, PK
LocationId: FK
Unique Index on LocationId/IsSingleLevelScore
I imported that into my EDMX - and included the LocationId FK (i never usually do this - but it's required in this instance).
Created the derived entities, mapped the fields, set the discriminator.
Created an association between Location -> LocationOverallScore, Location -> LocationScore (based on LocationId referential constraint).
All works fine.
The one downside is that because LocationId is not part of the PK on the LocationScore table, it's a 1..* between Location and LocationOverallScore, when in reality it should only be a 1..1.
I enforce this business requirement in the model via a hook property:
public class Location
{
// EF navigational properties - required
public ICollection<LocationOverallScore> LocationOverallScores { get; set; }
public ICollection<LocationSingleLevelScore> LocationSingleLevelScores { get; set; }
// Hook properties
public LocationOverallScore OverallScore
{
get { return LocationOverallScores.SingleOrDefault(); }
}
public LocationSingleLevelScores SingleLevelScore
{
get { return LocationSingleLevelScores .SingleOrDefault(); }
}
}
So the .SingleOrDefault() will throw an exception if there is more than one record - which there never will be, because of the unique index.
So i can now do this:
var location = ctx.Locations.Include("LocationOverallScores").Single();
var overallScore = location.OverallScore;
That's what i'll be going with for now.