Pros and cons of identifying relationship over non-identifying and vice versa - php

Let's imagine simple real world customer-loan relationship scenario, where loan existence without customer is impossible, hence the relationship logically should be many-to-one identifying relationship with the following structure:
CREATE TABLE `customer` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50)
) ENGINE = InnoDB;
CREATE TABLE `loan` (
`id` INT NOT NULL AUTO_INCREMENT,
`customer_id` INT NOT NULL,
`amount` FLOAT,
`currency` VARCHAR(10),
PRIMARY KEY (`id`, `customer_id`),
CONSTRAINT `identifying_fk` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`id`)
) ENGINE = InnoDB;
On the other hand, the same logic technically can be applied with many-to-one non-identifying mandatory relationship with the following structure:
CREATE TABLE `customer` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50)
) ENGINE = InnoDB;
CREATE TABLE `loan` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`customer_id` INT NOT NULL,
`amount` FLOAT,
`currency` VARCHAR(10),
CONSTRAINT `non-identifying_fk` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`id`)
) ENGINE = InnoDB;
Question: What are the advantages and disadvantages of using identifying relationship over non-identifying relationship or vice versa? Are there any technical preferences choosing one over another?
NB. One of the disadvantage using identifying relationship is composite PRIMARY KEY, which are generally difficult to maintain.
For example PHP Doctrine ORM does not support operating on such composite key, where one id is auto generated and the second key (foreign key) is the identifier of parent entity.

If you have an auto_increment column, then that should be the primary key. In general, I avoid composite primary keys. They just introduce scope for error in foreign key definitions and join conditions. You also point out the limitation when using other tools.
I would expect this question for an n-m relationship. That is one case where there is a good argument for a composite primary key. However, in your case, loans have only one customer, so the second method seems more "correct".

Meanwhile I read about the difference between identifying relationships and non-identifying relationships.
In your example, you have a many to one relationship. As such, the loans do not qualify for an identifying relationship, because the customer id is not sufficient to identify a loan. Thus the relationship is non-identifying.
If each customer can have only one loan, there would be a one to one relationship between the loans and the customers. The customer id would be sufficient to identify a loan, thus we have an identifying relationship. In this case, it would be a good choice to set the customer_id column of the loans table as a primary key.
Identifying relationships are also used with the link table in a many to many relationship.

Related

MYSQL table wont allow multiple foreign keys

I know this has been asked again and again, and I've tried so many times and don't understand why I keep getting errors, but I'm trying to connect the order details table to the order items, users and payment table, but SQL is coming up with. (this is for a school project)
I've been able to connect a table with two constraints but never with three.
#1005 - Can't create table oursmall.order_details (errno: 150 "Foreign key constraint is incorrectly formed")
CREATE TABLE IF NOT EXISTS order_details(
order_details_id INT(10) NOT NULL AUTO_INCREMENT,
order_items_id INT(10) NOT NULL,
users_id INT(10) NOT NULL,
total DECIMAL(6,2) NOT NULL,
payment_id INT(10) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(order_details_id),
CONSTRAINT fk_order FOREIGN KEY(order_items_id) REFERENCES order_items(order_items_id),
CONSTRAINT fk_users FOREIGN KEY(users_id) REFERENCES users(users_id),
CONSTRAINT fk_payment FOREIGN KEY(payment_id) REFERENCES users(payment_id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE="utf8_unicode_ci";
Thank you!
The column(s) referenced by a foreign key must be a key of the referenced table. Either the primary key or at least a secondary unique key.*
CONSTRAINT fk_payment FOREIGN KEY(payment_id) REFERENCES users(payment_id)
Is payment_id really the primary or unique key of the users table? I would be surprised if it is.
The second foreign key references users.users_id, right? That's what I assume is the primary key of that table.
* InnoDB supports a non-standard feature to allow the referenced column to be any indexed column, even a non-unique one. But this is not the standard of foreign keys in the SQL language, and I don't recommend doing it. For example, if a foreign key references a value that may appear on multiple rows in the parent table, what does that mean? Which row is truly the parent row?

Best practice for setting non-normalized columns in Yii2

Question to all Yii2 normalization geeks out there.
Where is the best place to set non-normalized columns in Yii2?
Example, I have models Customer, Branch, CashRegister, and Transaction.
In a perfect world, and in a perfectly normalized Database, our Transaction model would have only the cashregister_id, The CashRegister would store branch_id, and the Branch would store customer_id. However due to performance issues, we find ourselves obliged sometimes though to have a non-normalized Transaction model containing the following:
cashregister_id
branch_id
customer_id
When creating a transaction, I want to store all 3 values. Setting
$transaction->branch_id = $transaction->cashRegister->branch_id;
$transaction->customer_id = $transaction->cashRegister->branch->customer_id;
however in the controller does not feel correct.
One solution would be to do this in aftersave() in the Transaction model and make those columns read-only. But this also seems better but not perfect.
I wanted to know what is the best practice or where is the best place to set those duplicate columns, to make sure that the data integrity is maintained?
The following is a DB-only solution.
I assume your relations are:
A customer has many branches
A branch has many cashregisters
A cashregister has many transactions
The corresponding schema could be:
create table customers (
customer_id int auto_increment,
customer_data text,
primary key (customer_id)
);
create table branches (
branch_id int auto_increment,
customer_id int not null,
branch_data text,
primary key (branch_id),
index (customer_id),
foreign key (customer_id) references customers(customer_id)
);
create table cashregisters (
cashregister_id int auto_increment,
branch_id int not null,
cashregister_data text,
primary key (cashregister_id),
index (branch_id),
foreign key (branch_id) references branches(branch_id)
);
create table transactions (
transaction_id int auto_increment,
cashregister_id int not null,
transaction_data text,
primary key (transaction_id),
index (cashregister_id),
foreign key (cashregister_id) references cashregisters(cashregister_id)
);
(Note: This should be part of your question - so we wouldn't need to guess.)
If you want to include redundant columns (branch_id and customer_id) in the transactions table, you should make them part of the foreign key. But first you will need to include a customer_id column in the cashregisters table and also make it part of the foreign key.
The extended schema would be:
create table customers (
customer_id int auto_increment,
customer_data text,
primary key (customer_id)
);
create table branches (
branch_id int auto_increment,
customer_id int not null,
branch_data text,
primary key (branch_id),
index (customer_id, branch_id),
foreign key (customer_id) references customers(customer_id)
);
create table cashregisters (
cashregister_id int auto_increment,
branch_id int not null,
customer_id int not null,
cashregister_data text,
primary key (cashregister_id),
index (customer_id, branch_id, cashregister_id),
foreign key (customer_id, branch_id)
references branches(customer_id, branch_id)
);
create table transactions (
transaction_id int auto_increment,
cashregister_id int not null,
branch_id int not null,
customer_id int not null,
transaction_data text,
primary key (transaction_id),
index (customer_id, branch_id, cashregister_id),
foreign key (customer_id, branch_id, cashregister_id)
references cashregisters(customer_id, branch_id, cashregister_id)
);
Notes:
Any foreign key constraint needs an index in the child (referencing) and the parent (referenced) table, which can support the constraint check. The given column order in the keys allows us to define the schema with only one index per table.
A foreign key should always reference a unique key in the parent table. However in this example the composition of referenced columns is (at least) implicitly unique, because it contains the primary key. In almost any other RDBMS you would need to define the indices in the "middle" tables (branches and cashregisters) as UNIQUE. This however is not necessary in MySQL.
The composite foreign keys will take care of the data integrity/consistency. Example: If you have a branch entry with branch_id = 2 and customer_id = 1 - you wan't be able to insert a cashregister with branch_id = 2 and customer_id = 3, because this would violate the foreign key constraint.
You will probably need more indices for your queries. Most probably you will need cashregisters(branch_id) and transactions(cashregister_id). With these indices you might not even need to change your ORM relation code. (though AFAIK Yii supports composite foreign keys.)
You can define relations like "customer has many transactions". Previously you would need to use "has many through", involving two middle/bridge tables. This will save you two joins in many cases.
If you want the redundant data to be maintained by the database, you can use the following triggers:
create trigger cashregisters_before_insert
before insert on cashregisters for each row
set new.customer_id = (
select b.customer_id
from branches b
where b.branch_id = new.branch_id
)
;
delimiter $$
create trigger transactions_before_insert
before insert on transactions for each row
begin
declare new_customer_id, new_branch_id int;
select c.customer_id, c.branch_id into new_customer_id, new_branch_id
from cashregisters c
where c.cashregister_id = new.cashregister_id;
set new.customer_id = new_customer_id;
set new.branch_id = new_branch_id;
end $$
delimiter ;
Now you can insert new entries without defining the redundant values:
insert into cashregisters (branch_id, cashregister_data) values
(2, 'cashregister 1'),
(1, 'cashregister 2');
insert into transactions (cashregister_id, transaction_data) values
(2, 'transaction 1'),
(1, 'transaction 2');
See demo: https://www.db-fiddle.com/f/fE7kVxiTcZBX3gfA81nJzE/0
If your business logic allows to update the relations, you should extend your foreign keys with ON UPDATE CASCADE. This will make the changes through the relation chain down to the transactions table.
I had similar problem once and using afterSave() or beforeSave() looked as a great solution at the beginning, but finally resulted hard to maintain spaghetti code. I ended up with creating separate component for managing such relations. Something like:
class TransactionsManager extends Component {
public function createTransaction(TransactionInfo $info, CashRegister $register) {
// magic
}
}
Then you're not creating or updating Transaction model directly, you're alway using this component and encapsulates all logic in it. Then ActiveRecord works more like a data representation and does not contain any advanced business logic. It looks more complicated in some cases than $model->load($data) && $model->save() but after all it is much easier to maintain when you have all logic in one place and you don't need to debug save() calls chains (one model runs save() of different model in afterSave() which runs save() of different model in afterSave()... and so on).

Doctrine, how to left join an entity which wasn't generated?

I have rights:
CREATE TABLE `rights` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE INDEX `U_name` (`name`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
and profiles:
CREATE TABLE `profile` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE INDEX `U_name` (`name`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
I want to connect profiles to rights and also profiles to profiles:
CREATE TABLE `profile_profile` (
`profile_id1` INT(10) UNSIGNED NOT NULL DEFAULT '0',
`profile_id2` INT(10) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY (`profile_id1`, `profile_id2`),
INDEX `I_profile_id2` (`profile_id2`),
CONSTRAINT `FK_profile_profile-profile-1` FOREIGN KEY (`profile_id1`) REFERENCES `profile` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `FK_profile_profile-profile-2` FOREIGN KEY (`profile_id2`) REFERENCES `profile` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
CREATE TABLE `profile_right` (
`profile_id` INT(10) UNSIGNED NOT NULL DEFAULT '0',
`right_id` INT(10) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY (`profile_id`, `right_id`),
INDEX `I_right_id` (`right_id`),
CONSTRAINT `FK_profile_right-profile` FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `FK_profile_right-rights` FOREIGN KEY (`right_id`) REFERENCES `rights` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
a better overview:
so I generate entities:
php apps/doctrine.php dev orm:generate-entities libs/ --no-backup
--extend="\Doctrine\Entity\BaseEntity"
here come the problems. The Profile and Rights entities gets created, while Profile_rights and Profile_profile not. How to use them then?
In doctrine, join tables are not represented by an Entity.
You can find a #ORM\ManyToMany in your entities, with a #ORM\JoinTable and all informations about your associations.
This is the representation of your join table(s), use getters and setters like said by #Richard to access them.
Get more informations in the Associations mapping (to see all kind of associations) and Working with associations (to learn how work with them) chapters of the documentation.
Hope you have a good experience with doctrine.
EDIT
After look more at your UML, at least one of your associations doesn't need a many-to-many (As said by the first answer), but if they really have join tables in SQL, and you imported them by reverse engineering, they will surely be exactly as they are in SQL (a many-to-many with join table).
If you want to access the joining entity on a ManyToMany relationship you need to break it down to a OneToMany, ManyToOne.
E.g.
Profile - OneToMany < ProfileRight > ManyToOne - Profile.
Whether you should is another question. You only need to do this if you want to store extra data on the join table.
With what you have there it's trivial to get rights for a profile. For any profile you have loaded you simply call
$profile->getRights()
and doctrine will (assuming your generated entity mappings are correct) transparently fetch all the associated Rights entities for you based on the join table.
Similarly if you add a Right to a profile:
$right = new Right();
$profile->addRight($right);
Doctrine will transparently add the join table entry for you.

Creating foreign keys in phpMyAdmin (MySQL)

I'm trying to create a database in MySQL on phpMyAdmin. I am able to create the tables without any trouble, but I also want to add some foreign keys. In this case I want to link the BIDS and CLIENTS tables via the CLIENTID attribute.
CREATE TABLE BIDS (
BIDID NUMERIC(3) NOT NULL PRIMARY KEY,
CLIENTID NUMERIC(3) NOT NULL
);
CREATE TABLE CLIENTS (
CLIENTID NUMERIC(3) NOT NULL,
EMAILADDRESSES VARCHAR(100) NOT NULL,
PHONENUMBERS VARCHAR(11) NOT NULL,
FOREIGN KEY (CLIENTID) REFERENCES BIDS (CLIENTID),
PRIMARY KEY (CLIENTID,EMAILADDRESSES,PHONENUMBERS)
);
Research has told me that the syntax is correct, but this code returns the following error.
1005 - Can't create table 'CLIENTS' (errno: 150)
Apparently, a solution might be involved with something called 'InnoDB'. How can I use it to fix my problem?
Syntax is fine but problem is with FORIEGN KEY statement as below. You can't create FK on a non-key column. In BIDS table it's BIDID which is defined as Primary Key and not CLIENTID
FOREIGN KEY (CLIENTID) REFERENCES BIDS (CLIENTID)
So, your FORIEGN KEY definition should actually be
FOREIGN KEY (CLIENTID) REFERENCES BIDS (BIDID)
See a demo here http://sqlfiddle.com/#!2/f1c9ec

Making advance search

I have one field in my database : xfield
This field has the following format for my posts(products) in website :
weight|3kg|year|2009|brand|samsung|monitorsize|13"|modem|yes
Now I want to perform an advance search. For example I want to search monitor sizes between 13" ~ 15"
and weight between 1.5kg ~ 3.5kg
How can I make that search page with php?
You are using CSV data in a database, this is a really really bad idea, that will make you bold (insert random picture of person pulling out hair here).
Never use CSV in databases, it's an anti-pattern.
Instead what you need to do is to refactor your DB design to only put one value in one field.
I'm guessing you want to be as flexible as possible.
In that case use entity-attribute-value model (EAV).
Your table should look like:
table properties (
id unsigned integer auto_increment primary key,
product_id unsigned integer,
attribute varchar(20) not null,
astring varchar(100),
anumber decimal(10,2),
atype enum('string','integer','boolean','float'),
unit varchar(10) not null,
foreign key (product_id) references product(id) on update cascade on delete cascade,
foreign key (attribute) references attribute(name) on update cascade on delete cascade,
foreign key (unit) references unit(name) on update cascade on delete cascade
) ENGINE = InnoDB;
table unit (
name varchar(10) primary key -- "kg","inch",.......
) ENGINE = InnoDB;
table attribute (
name varchar(20) primary key -- allowed name for the attribute
) ENGINE = InnoDB;
The links to the external tables ensure that your units and attributes are selected from a limited pool of consistent identifiers.
Now you can query your DB using something like this:
SELECT p.id, p.name
FROM product p
INNER JOIN properties ps1 ON (ps1.product_id = p.id)
INNER JOIN properties ps2 ON (ps2.product_id = p.id)
WHERE ps1.attribute = 'monitorsize' AND ps1.anumber BETWEEN 13 AND 15
WHERE ps2.attribute = 'weight' AND ps2.anumber BETWEEN 1.5 AND 3.5

Categories