OneToMany mapping from Table to Foreign Table [Symfony 2 / Doctrine] - symfony

From what I've gathered, Symfony 2 / Doctrine uses the database definitions (foreign key constraints in my case) to map relations between entities. I have two tables in particular that I want to be able to relate from both sides, but I do not want to create redundant foreign keys in each table. In this case, I have an Account table, and a Transaction table.
Account Table
CREATE TABLE "account" (
"account_id" BIGSERIAL NOT NULL,
"name" VARCHAR (100) NOT NULL,
"date_created" TIMESTAMP (6) WITH TIME ZONE NOT NULL,
"date_modified" TIMESTAMP (6) WITH TIME ZONE,
CONSTRAINT "pk-account-account_id"
PRIMARY KEY ("account_id"),
);
Transaction Table
CREATE TABLE "transaction" (
"transaction_id" BIGSERIAL NOT NULL,
"account_id" BIGINT NOT NULL,
"amount" MONEY NOT NULL,
"date_created" TIMESTAMP (6) WITH TIME ZONE NOT NULL,
"date_modified" TIMESTAMP (6) WITH TIME ZONE,
CONSTRAINT "pk-transaction-transaction_id"
PRIMARY KEY ("transaction_id"),
CONSTRAINT "fk-transaction-account_id-account-account_id"
FOREIGN KEY ("account_id")
REFERENCES "account" ("account_id")
ON DELETE RESTRICT
ON UPDATE CASCADE,
);
When I generate the entities using php bin/console doctrine:generate:entities I see that the transaction entity has an $account property, but my account entity does not have a $transaction entity. I assume this is because I do not define a foreign key constraint in my account table.
In my code, I create my account object by the following:
$accounts = $this->getDoctrine()
->getRepository('BalancesBundle:Account')
->findAll();
I then would want to iterate over the array to get the total balance for each account. In the long-term, I'd like to create a helper method inside my account entity that would call getTransactions() to add up all of the transactions into one sum total.
Is this possible? I feel like I'm missing something, and that my only recourse would be to do this from within the transaction entity. I would like to avoid doing this from the transaction entity if possible.

From what I get, you can't get transaction entities of the account entity. The weird thing is that you don't have "transactions" property inside your ccount entity, am I right ? Seems like a bad mapping.
Take a look at Doctrine documentation, you have to define your "transcations" property inside your "account" entity and map it with the OneToMany association.
Then you can use "php bin/console do:ge:entities AppBundle:Account"

If your entities are setup correctly then you should be able to access the transactions from the account.
foreach ($accounts as $account) {
$transactions = $account->getTransactions();
$transaction_total = 0;
foreach($transactions as $transaction) {
$transaction_total += $transaction->getAmount();
}
echo 'Transactions Total: '.$transaction_total;
}

Related

Wordpress table key relationships

I am writing a Wordpress plugin extending Woocommerce that requires to store additional information about a user.
I understand that there is a separate wp_user_meta table specific to this, but I would rather use a custom table as I plan to use Wordpress/Woocommerce as an initial platform then hopefully expand to others. The data will also be queried often.
This custom table requires a single entry per user, defined by the ID in wp_user, therefore essentially being a one-to-one relationship. An example schema for the custom table is:
CREATE TABLE {$wpdb->prefix}custom_table (
custom_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
custom_data varchar(200) NULL,
FOREIGN KEY (user_id) REFERENCES wp_users(ID),
PRIMARY KEY (custom_id)
)
My question is using Wordpress, how do you ensure that the one-to-one relationship is maintained?
I know I will have to run an initial setup to create the entries in the custom table for existing users.
Other than this, are users allowed to change their ID at all in the user lifecycle? Is it a case of using registration/deletion hooks to ensure that the data in the custom table is up to date? (Or would the deletion cascade through to other tables when it happens in the wp_user table)
AFAIK, users can't change their IDs nor their usernames (because, well, that leads to a whole lot of issues) so you're fine in that regard.
Also, there are a few "gotchas!" you need to be aware of:
Your query will fail because user_id isn't being defined exactly the same way as the ID column in wp_users. You have: user_id BIGINT UNSIGNED NOT NULL. It should be: user_id BIGINT(20) UNSIGNED NOT NULL.
To make user_id sure is unique -and maintain the one-to-one relationship- you'll need to add UNIQUE(user_id) to your query.
Your table has to use the exact same storage engine. In your query, you're not defining a storage engine for it so MySQL will fallback to the default one, which might or might not be the same one used by the wp_users table. If it's not the same one, the query will -again- fail and your table won't be created.
Here's the revised query, assuming wp_users uses the MyISAM storage engine:
CREATE TABLE {$wpdb->prefix}custom_table (
custom_id bigint unsigned NOT NULL AUTO_INCREMENT,
user_id bigint(20) unsigned NOT NULL,
custom_data varchar(200) NULL,
FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE,
PRIMARY KEY (custom_id),
UNIQUE (user_id)
) ENGINE=MyISAM
If you want to make sure your table uses the exact same storage engine as the wp_users table -and you do- running this query before creating your table will tell you which one you need:
SELECT ENGINE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA='{$wpdb->dbname}' AND TABLE_NAME='{$wpdb->users}';
For reference the code snippet - note that checks should be in place for checking the array size etc. but this gets me in the right direction. Thank you #cabrarahector
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
global $wpdb;
$query = "SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA='{$wpdb->dbname}' AND TABLE_NAME='{$wpdb->users}';";
$engine = $wpdb->get_results( $query )[0]->ENGINE;
$sql = "CREATE TABLE {$wpdb->prefix}custom_table (
custom_id bigint unsigned NOT NULL AUTO_INCREMENT,
user_id bigint(20) unsigned NOT NULL,
custom_data varchar(200) NULL,
FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE,
PRIMARY KEY (custom_id),
UNIQUE (user_id)
) ENGINE={$engine};";
dbDelta($sql);
this creates the custom table in the database

Database-level constraint to make sure product and tag belong to same account?

Suppose I have these tables:
CREATE TABLE "account" (
"id" INTEGER PRIMARY KEY
-- more fields
);
CREATE TABLE "product" (
"id" INTEGER PRIMARY KEY,
"accountId" INTEGER NOT NULL,
-- more fields
CONSTRAINT "fk_product_accountId" FOREIGN KEY("accountId") REFERENCES "account"("id") ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE "tag" (
"id" INTEGER PRIMARY KEY,
"accountId" INTEGER NOT NULL,
-- more fields
CONSTRAINT "fk_tag_accountId" FOREIGN KEY("accountId") REFERENCES "account"("id") ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE "product_tag" (
"productId" INTEGER NOT NULL,
"tagId" INTEGER NOT NULL,
PRIMARY KEY("productId", "tagId"),
CONSTRAINT "fk_tag_productId" FOREIGN KEY("productId") REFERENCES "product"("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "fk_product_tagId" FOREIGN KEY("tagId") REFERENCES "tag"("id") ON UPDATE CASCADE ON DELETE CASCADE
);
... in other words, an account can have products and tags associated with the account and can then associate these tags with those products.
Is there a way to define a constraint, at the database-level, that checks whether a product_tag combination is valid, i.e. whether the tag and product both belong to the same account, such that I can circumvent having to check this validity during an INSERT statement?
I thought about an additional column accountId in product_tag, but I don't think I can define multiple foreign keys on this, nor do I think this would actually check the constraint the way I intend.
Do CHECK constraints offer this level of complexity, perhaps?
How about this?
Account:
AccountID int not null
Tag:
AccountID int not null
TagID int not null
primary key (AccountID, TagID)
foreign key (AccountID) references Account(AccountID)
Product:
AccountID int not null
ProductID int not null
primary key (AccountID, ProductID)
foreign key (AccountID) references Account(AccountID)
ProductTag:
AccountID int not null,
TagID int not null,
ProductID int not null,
primary key(AccountID, TagID, ProductID)
foreign key(AccountID, TagID) references Tag(AccountID, TagID)
foreign key(AccountID, ProductID) references Product(AccountID, ProductID)
Using composite keys instead of identity keys on Tag and Product enables us to do this. I find using identity keys (surrogate keys) can have the effect of limiting the usefulness of constraints since the 'information' contained in the tables is more 'spread out', and constraints can only work on one or two tables, loosely speaking. I really like this scenario as an example of this effect. Using the first design would have forced us to use a trigger to enforce the rule.
I'm interested to see others' solutions on this...
A foreign key constraint cares only about the column(s) that are part of the constraint, so the only way to check the account ID would be to make it part of all keys.
CHECK constraints must not contain subqueries, so they cannot be used for this.
The only way to handle this with the existing database structure is a trigger:
CREATE TRIGGER check_product_tag_same_account
BEFORE INSERT ON product_tag
FOR EACH ROW
WHEN (SELECT accountId FROM product WHERE id = NEW.productId) !=
(SELECT accountId FROM tag WHERE id = NEW.tagId )
BEGIN
SELECT raise(FAIL, "account IDs do not match");
END;

Symfony2 Inserting two entities related using foreign key

I have a Product entity which have option named synchronization setting which are stored in another table. Product is related with SynchronizationSetting using OneToOne. I've a little problem with persisting new product. My annotations of SynchronizationSetting look like that:
/**
* #var \Webrama\ProductBundle\Entity\Product
*
* #ORM\OneToOne(targetEntity="Webrama\ProductBundle\Entity\Product")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_product", referencedColumnName="id")
* })
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $idProduct;
Here is the table structure:
CREATE TABLE IF NOT EXISTS `synchronization_setting` (
`id_product` int(10) unsigned NOT NULL,
`local` char(1) DEFAULT '0',
`internet` char(1) DEFAULT '0',
PRIMARY KEY (`id_product`),
KEY `id_product` (`id_product`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE `synchronization_setting`
ADD CONSTRAINT `fk_ss_id_product` FOREIGN KEY (`id_product`) REFERENCES `product` (`id`) ON UPDATE CASCADE;
I'm trying to insert new product with this code:
if($request->getMethod() == 'POST')
{
$form->handleRequest($request);
if($form->isValid())
{
$product = $form->getData();
$synchronizationSetting = $product->retSynchronizationSetting();
$synchronizationSetting->setIdProduct($product);
$em->persist($synchronizationSetting);
$em->flush();
$em->persist($product);
$em->flush();
$this->get('session')->getFlashBag()->add('notice', $this->get('translator')->trans('save_ok'));
return $this->redirect($this->generateUrl("webrama_product_index"));
}
}
The operations fails becouse product does no have related synchronization setting entity at the moment of inserting. The error is:
Entity of type Webrama\ProductBundle\Entity\SynchronizationSetting has identity through a foreign entity Webrama\ProductBundle\Entity\Product, however this entity has no identity itself. You have to call EntityManager#persist() on the related entity and make sure that an identifier was generated before trying to persist 'Webrama\ProductBundle\Entity\SynchronizationSetting'. In case of Post Insert ID Generation (such as MySQL Auto-Increment or PostgreSQL SERIAL) this means you have to call EntityManager#flush() between both persist operations.
What I have to to is:
Create synchronization setting entity with idProduct before I insert the product,
Insert new Product to database
To do that I would make an insert to synchronization setting table before persisting the product but I'm pretty sure the is a better way to do that using Product and SynchronizationSetting entities given me by form. The question is: what way?
I'm afraid the easiest way to go around this will be to just flush product to DB so it gets it's primary key. After that relate synchronization to already flushed product and flush synchronization itself.

Many-to-one custom fields - unique

I'm using a foreign table to join 2 entities, exactly this way :
http://www.prowebdev.us/2012/07/symfnoy2-many-to-many-relation-with.html
I'd like to understand why do we need an ID as primary key in the foreign table ?
I'd rather take the couple of foreign keys as the primary keys, this way I make sure that there is no double entries for the same relation. no ?
I think Doctrine just wants every table to have and Id field, you can however force Doctrine the check if the combination of foreign keys is unique:
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
...
/*
* #UniqueEntity({"foreign_key1", "foreign_key2"})
* ...
*/
class JoinTable
{
...
}

How can i insert data from one table to another in symfony2 doctrine2

I have table called
tasks---id ,name,groupid
user----id,username , groupid
Now i have another table
userTasks with fields id,name, taskid , userid
Now i want that when the new user is created or persisted in database then
all the tasks with same groupid asuser should gets inserted in
usertasks table with taskid and userid
HOw can i do that
I am thinking of puting the logic in postFlush() event
But i don't know how doctrine first select and then insert the records in that table
If your do it just after of insert the data, instead inside postFlush function, could be more confortable and easy.
$om->persist($user);
$om->persist($task);
$om->flush();
$usertask = new UserTask($id, $name, $user->getId(), $task->getId());
$om->flush();
$om is the ObjectManager; EntityManager is deprecated in Doctrine last version.
If you want to use postFlush() function, maybe have to declare both id's as private variables to have the info.

Resources