17

I am designing a recipe database that needs to be very flexible as it is going to be communicating directly with our back-of-house inventory system. This is what I have so far in regards to the tables:

  • Recipe: this table will contain the recipe date: the name, steps needed to cook, etc.
  • Ingredients/Inventory: this is our back of house inventory, so this will have the information about each product that will be used in our recipes.
  • Recipe Line Item: This is the tricky table, I want to be able link to the ingredients here as well as the quantity needed for the recipe, but I also need to be able to directly include recipes from the recipe table (such as marinara sauce that we make in-house), and that is why I am having trouble figuring out the best way to design this table.

Basically, the recipe line item table needs to be able to link to either the ingredients table or the recipe table depending on which line item is needed and I want to know what would be the most effective way to handle that.

Thank you so much in advance!

VinceL
  • 189
  • 1
  • 1
  • 3

5 Answers5

8

Looks like you need a database model similar to this:

enter image description here

This model has the following properties:

  • Essentially, each recipe is a series of steps.
  • Each step has its order relative to other steps of the same recipe (STEP_NO), a unit (mass, volume, count...), a quantity in that unit etc.
  • A particular step is connected either to an ingredient (when INGREDIENT_ID is non-NULL) or to another recipe (when SUBRECIPE_ID is non-NULL).1
  • Other than that, the STEP is a fairly standard junction table implementing many-to-many relationship, which means the same ingredient can be used in multiple recipes (or even multiple steps of the same recipe) and also a recipe can be a "sub-recipe" of multiple other recipes.
  • This is essentially a directed graph. The data model itself will not prevent cycles - they should be avoided at the client code level and possibly detected by triggers.

1 If MySQL supported CHECK constraints (which it doesn't), you could ensure that one (but not both) of them is non-NULL like this:

CHECK (
    (INGREDIENT_ID IS NULL AND SUBRECIPE_ID IS NOT NULL)
    OR (INGREDIENT_ID IS NOT NULL AND SUBRECIPE_ID IS NULL)
)

As it stands, you'll need a trigger for that.

Community
  • 1
  • 1
Branko Dimitrijevic
  • 50,809
  • 10
  • 93
  • 167
  • Branko, are you still active here? That would be great! I have a question regarding your answer. Thank you very much. – Phantom Feb 15 '21 at 20:25
  • @Phantom Ask away! :) – Branko Dimitrijevic Feb 15 '21 at 23:32
  • @BrankoDimitrijevic Why did you propose to store the unit variable in the Step table instead of the Ingredient table? I feel like each time you'll create a new Step you must specify the unit, whereas if it was defined in the Ingredient you'll only need the quantity for a new step. – elkd Oct 26 '22 at 21:30
  • @elkd Good point. What we actually need is dimension (mass, volume) in INGREDIENT and unit of that dimension (mass: mg, g, kg..., volume: ml, dl, l...) in STEP. – Branko Dimitrijevic Oct 27 '22 at 09:06
3

Ingredients and Recipes are both possible RecipeItems:

CREATE TABLE RecipeItems (
  ItemID       SERIAL,
  Type         ENUM('Ingredient', 'Recipe'),
  Name         VARCHAR(255) NOT NULL,
  Quantity     FLOAT NOT NULL,
  INDEX (ItemID, Type)
);

CREATE TABLE Ingredients (
  IngredientID BIGINT UNSIGNED NOT NULL,
  Type         ENUM('Ingredient'),
  CostPrice    DECIMAL(6,2),
  PRIMARY KEY (IngredientID),
  FOREIGN KEY (IngredientID, Type) REFERENCES RecipeItems (ItemID, Type)
);

CREATE TABLE Recipes (
  RecipeID     BIGINT UNSIGNED NOT NULL,
  Type         ENUM('Recipe'),
  SellPrice    DECIMAL(6,2),
  Date         DATE,
  Instructions TEXT,
  PRIMARY KEY (RecipeID),
  FOREIGN KEY (RecipeID, Type) REFERENCES RecipeItems (ItemID, Type)
);

Then RecipeLineItems:

CREATE TABLE RecipeLineItems (
  RecipeID     BIGINT UNSIGNED NOT NULL,
  ItemID       BIGINT UNSIGNED NOT NULL,
  Quantity     FLOAT NOT NULL,
  PRIMARY KEY (RecipeID, ItemID),
  FOREIGN KEY (RecipeID) REFERENCES Recipes     (RecipeID),
  FOREIGN KEY (ItemID)   REFERENCES RecipeItems (ItemID)
);

With this approach, I recommend that you enable strict SQL mode (or else invalid values will be accepted in the ENUM typed columns with the empty string '' as a special error value): this could break the intended referential integrity of the above model. An alternative (but slightly more tedious) approach would be to enforce referential integrity manually using triggers.

If only MySQL supported CHECK constraints, huh?

eggyal
  • 122,705
  • 18
  • 212
  • 237
  • Hi eggyal, I am going to be honest with you, I have never used a foreign key in the manner that you are using it on the Recipes and Ingredients tables to link back to the RecipeItems table, specifically this line: `FOREIGN KEY (IngredientID, Type) REFERENCES RecipeItems (ItemID, Type)` I usually implement them in a manner similar to how you set it up on the RecipeLineItems table. So my question is, will an Ingredient still be reusable for other recipes using this setup? Thank you so much for your response! – VinceL Dec 14 '12 at 17:32
  • @VinceL: All the `FOREIGN KEY` does is constrain permissible records to those where the listed local columns match those in the referenced table. In this case, records in the `Ingredients` table must match `RecipeItems` on both the `ID` *and* the `Type` (but since `Ingredients.Type` is restricted to the value `'Ingredient'`, it thereby ensures that an `Ingredient` cannot be a `Recipe`; the converse is true of the `Recipes` table). Since `RecipeLineItems.ItemID` references `RecipeItems.ItemID`, but without any constraint on `RecipeItems.Type`, it can reference both `Ingredients` and `Recipes`. – eggyal Dec 14 '12 at 17:46
  • Okay, I think I got a grasp of it now, and I think this is very good solution. I do have one other question, what is the purpose of having the quantity field on both the `RecipeItems` table and the `RecipeLineItems` table? I can see why its needed on the `RecipeLineItems` table, but am not sure of what it's usage would be on the `RecipeItems` table. Thanks again for your clarification! – VinceL Dec 14 '12 at 17:52
  • @VinceL: How does one know what quantity of marinara sauce is produced by its recipe, or what quantity of soy sauce comes in a bottle? – eggyal Dec 14 '12 at 18:14
  • Okay, I gotcha, I was originally going to be putting the quantity field in the actual ingredient and recipe tables, but it does make more sense to store it in the one table. Thank you again, this is definitely much better than the solutions I had come up with so far! – VinceL Dec 14 '12 at 18:18
1

This script will create you a database which let you manage recipes, ingredients, the recip composition (ingredients_recipes) and also unities for you inventory and the composition. It also let you manage your inventory history.

Here is the query to get your current recipe, the ingredients needed, the quantity needed and the stock you currently have :

SELECT recipes.id, recipes.name AS recipeName, ingredients.name AS ingredientNeeded, CONCAT(ingredients_recipes.Qty,' ',neededUnities.name) AS neededQuantity, CONCAT(inventories.qty,' ',inventoryUnities.name) AS availableQuantity FROM recipes 

LEFT JOIN ingredients_recipes ON recipes.id=ingredients_recipes.recipe_id 
LEFT JOIN ingredients ON ingredients_recipes.ingredient_id = ingredients.id 
LEFT JOIN inventories ON ingredients.id=inventories.ingredient_id 
LEFT JOIN unities AS inventoryUnities ON inventories.unity_id=inventoryUnities.id
LEFT JOIN unities AS neededUnities ON ingredients_recipes.unity_id=neededUnities.id

WHERE inventories.`update` = (SELECT MAX(`update`) FROM inventories AS inv WHERE inv.ingredient_id = inventories.ingredient_id);

the database :

-- --------------------------------------------------------
-- Host:                         127.0.0.1
-- Server version:               5.5.16 - MySQL Community Server (GPL)
-- Server OS:                    Win32
-- HeidiSQL version:             7.0.0.4053
-- Date/time:                    2012-12-14 16:33:22
-- --------------------------------------------------------

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!40014 SET FOREIGN_KEY_CHECKS=0 */;

-- Dumping database structure for database
DROP DATABASE IF EXISTS `database`;
CREATE DATABASE IF NOT EXISTS `database` /*!40100 DEFAULT CHARACTER SET latin1 */;
USE `database`;


-- Dumping structure for table database.ingredients
DROP TABLE IF EXISTS `ingredients`;
CREATE TABLE IF NOT EXISTS `ingredients` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `name` varchar(250) NOT NULL,
  `unity_id` int(11) NOT NULL COMMENT 'for the default unity',
  `Created` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `Unity_id` (`unity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

-- Dumping data for table database.ingredients: ~0 rows (approximately)
DELETE FROM `ingredients`;
/*!40000 ALTER TABLE `ingredients` DISABLE KEYS */;
/*!40000 ALTER TABLE `ingredients` ENABLE KEYS */;


-- Dumping structure for table database.ingredients_recipes
DROP TABLE IF EXISTS `ingredients_recipes`;
CREATE TABLE IF NOT EXISTS `ingredients_recipes` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `ingredient_id` int(10) NOT NULL,
  `recipe_id` int(10) NOT NULL,
  `Qty` float NOT NULL,
  `Unity_id` int(10) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ingredient_id_recipe_id` (`ingredient_id`,`recipe_id`),
  KEY `Unity_id` (`Unity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

-- Dumping data for table database.ingredients_recipes: ~0 rows (approximately)
DELETE FROM `ingredients_recipes`;
/*!40000 ALTER TABLE `ingredients_recipes` DISABLE KEYS */;
/*!40000 ALTER TABLE `ingredients_recipes` ENABLE KEYS */;


-- Dumping structure for table database.inventories
DROP TABLE IF EXISTS `inventories`;
CREATE TABLE IF NOT EXISTS `inventories` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `ingredient_id` int(10) NOT NULL COMMENT 'ingredient',
  `qty` int(10) NOT NULL COMMENT 'quantity',
  `unity_id` int(11) NOT NULL COMMENT 'unity for the ingredient',
  `update` datetime NOT NULL COMMENT 'date of the inventory update',
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

-- Dumping data for table database.inventories: ~0 rows (approximately)
DELETE FROM `inventories`;
/*!40000 ALTER TABLE `inventories` DISABLE KEYS */;
/*!40000 ALTER TABLE `inventories` ENABLE KEYS */;


-- Dumping structure for table database.recipes
DROP TABLE IF EXISTS `recipes`;
CREATE TABLE IF NOT EXISTS `recipes` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `name` varchar(250) NOT NULL,
  `cooking` longtext NOT NULL,
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

-- Dumping data for table database.recipes: ~0 rows (approximately)
DELETE FROM `recipes`;
/*!40000 ALTER TABLE `recipes` DISABLE KEYS */;
/*!40000 ALTER TABLE `recipes` ENABLE KEYS */;


-- Dumping structure for table database.unities
DROP TABLE IF EXISTS `unities`;
CREATE TABLE IF NOT EXISTS `unities` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

-- Dumping data for table database.unities: ~0 rows (approximately)
DELETE FROM `unities`;
/*!40000 ALTER TABLE `unities` DISABLE KEYS */;
/*!40000 ALTER TABLE `unities` ENABLE KEYS */;
/*!40014 SET FOREIGN_KEY_CHECKS=1 */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
guignol
  • 404
  • 4
  • 10
  • I apologize for the delay responding, how would you use the name field on the unities table? At first I was thinking that you would use it to say what type of field you were unifying (be it a recipe, recipe_ingredient, or an ingredient), but then I saw that it was unique. Thanks for your response, it definitely looks interesting! – VinceL Dec 18 '12 at 18:53
  • name is just the name of the unity for instance L for Liter, mL for milliliter, g for grams and so on, in fact this is the "name" field is the display field for your "dropdown list". It has to be unique, imagine you want to compare the unity used in the recipes and the one you use for you inventory the reference to each unity has to be unique. just to make a parallelle, not being unique for this type of field would leave you for instance to an frontend app with 3 times "kg" in the dropdown list... which one will you choose ? the constraint unique won't let you duplicate such a data. – guignol Dec 18 '12 at 20:32
0

You’ll need three tables: a recipes table, an ingredients table, and a recipe_ingredients table that assigns ingredients to a recipe. You can also store additional information in this table, such as quantities. So for example, if you have a recipe for vegetable soup, you would have multiple entries for vegetables with the corresponding quantities. These entries would then be linked to the relevant recipe and ingredients via a foreign key.

EDIT: Schema at its simplest:

CREATE TABLE `ingredients` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(45) NOT NULL,
  PRIMARY KEY (`id`)
) TYPE=InnoDB;

CREATE TABLE `recipes` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(45) NOT NULL,
  PRIMARY KEY (`id`)
) TYPE=InnoDB;

CREATE TABLE `recipe_ingredients` (
  `recipe_id` int(10) unsigned NOT NULL,
  `ingredient_id` int(10) unsigned NOT NULL,
  `quantity` int(10) unsigned NOT NULL,
  KEY `recipe_id` (`recipe_id`),
  KEY `ingredient_id` (`ingredient_id`)
) TYPE=InnoDB;


ALTER TABLE `recipe_ingredients`
  ADD CONSTRAINT `recipe_ingredients_ibfk_2` FOREIGN KEY (`ingredient_id`) REFERENCES `ingredients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
  ADD CONSTRAINT `recipe_ingredients_ibfk_1` FOREIGN KEY (`recipe_id`) REFERENCES `recipes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
Martin Bean
  • 38,379
  • 25
  • 128
  • 201
  • so, in the recipe_ingredients table, you would have two nullable foreign keys, one to the ingredients table and one to recipe table? Then just populate whichever one is relevant to that recipe_ingredient? – VinceL Dec 14 '12 at 16:49
  • I wouldn’t have nullable foreign keys as the table would be assigning an ingredient to a recipe effectively, so both foreign keys will be populated. If you don’t have an ingredient, it can’t be assigned to a recipe. – Martin Bean Dec 14 '12 at 17:01
  • Using that schema, how does one use the "*marinara sauce*" (itself a recipe) as an ingredient in another recipe? – eggyal Dec 14 '12 at 17:08
  • I see. In that case, you could have two columns in your `recipe_ingredients` table: `ingredient_type` (an `ENUM` column with either ”ingredient“ or ”recipe“ as the value), and then a `item_id` column that holds the foreign key ID of the record, be it from the `ingredients` table or the `recipes` table. – Martin Bean Dec 14 '12 at 22:23
0

One way.

Dish
Key ID Name
1   1  Snails in Marinara Sauce
2   2  Marinara Sauce
3   3  Glass of Water


Ingredient
Key DISHID Name
1   NULL   Snail
2   NULL   Tomato
3   NULL   Onion
4   NULL   Evian
5   2      Marinara Sauce

Recipe
DishID IngredientKey Qty UOM
1      1             6   Each
1      5             3   TblSpoon
2      2             2   Each
2      3             1   Each
3      4             275 Millilitres

So if an ingredient is a dish, it has a recipe.

Amended after a question from OP, that pointed to a wee flaw in my potential answer.

Tony Hopkinson
  • 20,172
  • 3
  • 31
  • 39