Referencing doctrine reference - one to many unidirectional
class User
{
// ...
/**
* #ManyToMany(targetEntity="Phonenumber")
* #JoinTable(name="users_phonenumbers",
* joinColumns={#JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=true)}
* )
*/
private $phonenumbers;
// ...
}
The part I don't understand is unique=true. What does it do? The way I read it is ...
User has a Many to Many relationship with Phonenumber
it uses the join table users_phonenumbers
users_phonenumbers.user_id = users.id
users_phonenumbers.phonenumber_id = Phonenumber.id
and I guess the unique does something to constraints a many to many to a many to one relationship somehow. But how do you explain it? Also in a SQL sense (what is the output like)?
The mapping translates into the following SQL tables (assuming both have a surrogate ID, called id):
CREATE TABLE User (id INT(10) PRIMARY KEY)
CREATE TABLE Phonenumber (id INT(10) PRIMARY KEY)
CREATE TABLE User_Phonenumber (
user_id INT(10),
phonenumber_id INT(10),
PRIMARY KEY (user_id, phonenumber_id),
UNIQUE(phonenumber_id)
);
What this means in terms of your code:
$phonenumber = new Phonenumber();
$phonenumber->setNumber("123-4567890");
$user1->addPhonenumber($phonenumber);
$user2->addPhonenumber($phonenumber);
$entityManager->flush();
This would throw a unique constraint exception, you cannot add the same phonenumber to different users, because phonenumbers are unique (on the database level).
The unique constraints ensure that data contained in a column or a group of columns is
unique.
Be aware, two null values are NOT considered equal, so you can store two or more duplicate rows. The primary key is already unique, so you don't need to use for primary key columns. :)
P.
Related
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).
I've got a friendship table between users that looks like this.
CREATE TABLE user_relations (
pkUser1 INTEGER UNSIGNED NOT NULL,
pkUser2 INTEGER UNSIGNED NOT NULL,
pkRelationsType TINYINT UNSIGNED NOT NULL,
PRIMARY KEY(pkUser1,pkUser2),
FOREIGN KEY(pkuser1) references users(ID),
FOREIGN KEY(pkuser2) references users(ID),
FOREIGN KEY(pkRelationsType) references user_relations_type(ID)
);
pkRelationsType is a pointer to another table that defines the kind of relation the users have (friendship(1),pending(2) or blocked(3))
If user 1 is friend with user 2 I've got only one instance |1|2|1| and NOT also |2|1|1|.
The thing is, in order to block a user I have to keep in mind the relation can be already made (users can be already friends or even have the pending friendship petition) so I am trying to insert the data or update it if the relation does not exist already.
I have this for the friendship request send, but this just ignores the the insert if the data exists already.
INSERT INTO
user_relations(pkUser1,pkUser2,pkRelationsType)
SELECT * FROM (SELECT :sender0,:target0,2) AS tmp
WHERE NOT EXISTS
(SELECT pkUser1 FROM user_relations
WHERE
(pkUser1= :sender1 AND pkUser2=:target1) OR (pkUser1=:sender2 AND pkUser1=:target2) LIMIT 1)
Due to the nature of the table I cannot use INSERT ... ON DUPLICATE KEY UPDATE.
I've been thinking about handling it with PHP, searching for the relation and it's order if exists and then doing one thing or another but it seems like a waste of processing.
Please note that I'm not a MYSQL expert even though I've handled myself so far.
Hope I have explained myself well enough.
Thanks for the feedback.
From your description, it seems that you are only keeping the "latest" relationship. If this is the case, why don't you DELETE the relationship first, then INSERT the new one?
I need to store multiple id's in either a field in the table or add another table to store the id's in.
Each member will basically have favourite articles. Each article has an id which is stored when the user clicks on a Add to favourites button.
My question is:
Do I create a field and in this field add the multiple id's or do I create a table to add those id's?
What is the best way to do this?
This is a many-to-many relationship, you need an additional table storing pairs of user_id and article_id (primary keys of user and article tables, respectively).
You should create a new table instead of having comma seperated values in a single column.
Keep your database normalized.
You create a separate table, this is how things work in a relational database. The other solution (comma separated list of ids in one column) will lead to an unmaintainable database. For example, what if you want to know how many times an article was favorited? You cannot write queries on a column like this.
Your table will need to store the user's id and the article's id - these refer to the primary keys of the corresponding tables. For querying, you can either use JOINs or nested SELECT queries.
As lafor already pointed out this is a many-to-many relationship and you'll end up with three tables: user, article, and favorite:
CREATE TABLE user(
id INT NOT NULL,
...
PRIMARY KEY (id)
) ENGINE=INNODB;
CREATE TABLE article (
id INT NOT NULL,
...
PRIMARY KEY (id)
) ENGINE=INNODB;
CREATE TABLE favorite (
userID INT NOT NULL,
articleID INT NOT NULL,
FOREIGN KEY (userID) REFERENCES user(id) ON DELETE CASCADE,
FOREIGN KEY (articleID) REFERENCES article(id) ON DELETE CASCADE,
PRIMARY KEY (userID, articleID)
) ENGINE=INNODB;
If you then want to select all user's favorite articles you use a JOIN:
SELECT * FROM favorite f JOIN article a ON f.articleID = a.id WHERE f.userID = ?
If you want to know why you should use this schema, I recommend reading about database normilization. With multiple IDs in a single field you would even violate the first normal form and thus land in a world of pain...
How can I specify the columns that are used for the foreign key relation with Class Table Inheritance in Doctrine 2? For example, take the following two classes:
/**
* #Entity
* #InhertanceType("JOINED")
* #DiscriminatorColumn(name="type", type="string")
* #DiscriminatorMap("person" = "Person", "employee" = "Employee")
*/
class Person
{
/** #Id */
public $id;
/** #Column(type="string") */
public $ssn;
}
/** #Entity */
class Employee
{
/** #Column(type="decimal") */
public $salary;
}
With this, Doctrine expects a table structure somewhat along this:
CREATE TABLE `person` (
`id` INT(11) NOT NULL auto_increment,
`ssn` VARCHAR(255) default NULL,
PRIMARY_KEY(`id`)
)
CREATE TABLE `employee` (
`person_id` INT(11) NOT NULL,
`salary` DECIMAL(10,2) default NULL,
PRIMARY_KEY(`person_id`)
)
ALTER TABLE `employee`
ADD CONSTRAINT `person_fk` FOREIGN KEY (`person_id`)
REFERENCES `person` (`id`) ON DELETE CASCADE
There is a foreign key employee.person_id that points to person.id. But how can I tell Doctrine which columns to use? I assume that the person.id reference comes from the #Id annotation on the Person class, but what if I want to create the FK to person.ssn instead (it's conceivable, since SSN are naturally unique).
And what if I have a legacy database where employee.person_id is called employee.p_id instead?
First of all, you are missing the "extends Person" in the Employee class definition.
From what I know, when you are doing this type of inheritance, doctrine uses the primary key in the main class to join all other tables, its is expected behavior.
When you do a query, doctrine will join all the child tables to the parent and then hydrate according to the discriminator map. With that in mind, it doesn't matter if your primary key is an automatically incremental id or a unique field. If you have ssn defined in the parent class, it will be available for searching in all your subclasses.
So a couple of tips here. You can if you want, remove the automatical id and use ssn as your primary key, then doctrine expects that all of your child tables to have that same field defined in them. For performance it may be wise to have an integer instead of a 255 string to do the joins anyway.
If you want to mantain the automated id, you may want to add a unique index to the parent class, that way if you access the classes by that field, you wont get performance slowdowns.
If you want to have the class name to be something and the table name to be something else, use this
/** #Id #Column(name="p_id") */
public $id;
But then remember that all the tables that are part of the inheritance should use that name.
Moreover, I normally use doctrine for mapping an existing database and extending it (migrate to doctrine), and if I'm adding a new feature and the model requires so, I create the mapping and tables myself keeping in mind how Doctrines inheritance works. But if you have existing tables that think can be modeled with Inheritance, expect some trouble, and maybe the needs to modify the existing tables.
Hope this helps.
I have 3 tables
customer, menu, and order.
The order table is suppose to join the customer and menu tables, and contains the primary keys of both. Here's how I tried to create the order table on phpmyadmin.
create table order(
customerID int not null,
itemID int not null,
primary key (customerID, itemID),
foreign key(customerID) reference customer(ID),
foreign key(itemID) reference menu(itemID)
)
This doesn't work. What am I doing wrong?!!
order is a reserved word, try another name, or quote it, like
create table `order`(
customerID int not null,
itemID int not null,
primary key (customerID, itemID),
foreign key(customerID) reference customer(ID),
foreign key(itemID) reference menu(itemID) )
It is complaining as order is a reserved keyword. Wrapping it with backticks as #TokenMacGuy tells you to solves your problem. Here is a list of them
Furthermore, as a general rule, you can transform your entities like so to avoid problems, especially with reserved keywords:-
a) The Entity is always modeled (on paper) as singular as it represents a concept/asset/person in the real world or problem domain. eg. ORDER, CHECK, STUDENT, CAR
b) the corresponding DB Table it is transformed into can always be named using plural. The logic is that the table will contain lots of instances of that Entity. Therefore ORDERS, CHECKS, STUDENTS, CARS