Making HABTM relationships unique in CakePHP - php

I have two models, called Book and Tag, which are in a HABTM relationship. I want a couple (book, tag) to be saved only once. In my models I have
var $hasAndBelongsToMany = array(
'Tag' => array(
'className' => 'Tag',
'joinTable' => 'books_tags',
'foreignKey' => 'book_id',
'associationForeignKey' => 'tag_id',
'unique' => true
)
);
and viceversa, but the Unique flag does not help me; I can still save two times the same couple.
How do I do this in CakePHP? Should I declare the couple (book, tag) unique in the database directly, or will this make CakePHP go nuts? Is there a Cakey way to handle this situation?
EDIT: I tried making the couple unique with the query (I'm using MySQL)
ALTER TABLE books_tags ADD UNIQUE (book_id,tag_id);
but this does not work well. When I save more than one tag at a time, everything goes well if all the couples are new. If at least one of the couples is repeated, CakePHP fails to do the whole operation, so it does not save ANY new couple (not even the good ones).

The way Cake usually saves HABTM data is by erasing all existing data for the model from the HABTM table and inserting all the relationships anew from the data array you feed into save(). If you're exclusively using this way to deal with HABTM relationship, you shouldn't have a problem.
Of course, that's not always the ideal way. To enforce validation on the HABTM table, you need to create the model for it and add some custom validation, like this:
class BooksTag extends AppModel {
var $validate = array(
'book_id' => array('rule' => 'uniqueCombi'),
'tag_id' => array('rule' => 'uniqueCombi')
);
function uniqueCombi() {
$combi = array(
"{$this->alias}.book_id" => $this->data[$this->alias]['book_id'],
"{$this->alias}.tag_id" => $this->data[$this->alias]['tag_id']
);
return $this->isUnique($combi, false);
}
}
Just make sure to save data using saveAll($data, array('validate' => 'first')) to trigger the validation.
You may have to specify the BooksTag model explicitly in the relationship:
var $hasAndBelongsToMany = array(
'Tag' => array(
...
'with' => 'BooksTag'
)
);
If on top of that you're also enforcing the uniqueness on the DB level, all the better.

In my opinion, any time you have data requirements, those requirements should be specified at the data(base) level. Enforce your rules at the lowest possible level. If the application can enforce those rules as well, and you choose to do so, then I'd say it can't hurt. I'd advise against using an application as the sole means of enforcing data constraints, though.

I would recommend that you put the constraint in the database schema. It would be easy if you could declare the combined columns as the primary key although I think this would cause cake some problems.
Depending on which RDBMS you use, you may be able to create a combined row index and declare a unique constraint on that.
You other option, for keeping this logic in Cake, would be to alter the beforeSave() function in the Book model, so that it first runs a find() on the passed data, and will only save if the find returns false (meaning there is no previous pair, meaning you've fulfilled your unique constraint).
One option that I don't promise will work, but looks like it might, would be the CakePHP Counter Cache behavior. Not sure it works by default with HABTM, but here is the link to the cake book (http://book.cakephp.org/view/816/counterCache-Cache-your-count) and the link to the addon that will make it work with habtm (http://bakery.cakephp.org/articles/view/counter-cache-behavior-for-habtm-relations)

Hackish way:
If you can have a third DB field (like the one above) do a varchar(32) that is UNIQUE that is an MD5 hash of the first two.
You'd have to modify all saves to be $md5_unique_field = md5( $field_one.$field_two );
CakePHP might allow for a custom validation or a custom model extension that could do this automatically.
I agree that this should be enforced at the DB level also though, and CakePHP should catch the errors.

Related

Swapping array values with Eloquent model IDs

I need to make an import method that takes the CSV file and imports everything in the database.
I've done the parsing with one of Laravel's CSV addons and it works perfectly giving me a big array of values set as:
[
'col1_name' => 'col1 value',
'col2_name' => 'col2 value',
'col3_name' => 'col3 value,
'...' => '...'
]
This is also perfect since all the column names fit my model which makes the database inserts a breeze.
However - a lot of column values are strings that i'd like to set as separate tables/relations. For example, one column contains the name of the item manufacturer, and i have the manufacturer table set in my database.
My question is - what's the easy way to go through the imported CSV and swap the strings with the corresponding ID from the relationship table, making it compatible with my database design?
Something that would make the imported line:
[
'manufacturer' => 'Dell',
]
into:
[
'manufacturer' => '32',
]
I know i could just do a foreach loop comparing the needed values with values from the relationship models but I'm sure there's an easier and more clean way of doing it.
I don't think theres any "nice" way to do this - you'll need to look up each value for "manufacturer" - the question is, how many queries will you run to do so?
A consideration you need to make here is how many rows you will be importing from your CSV file.
You have a couple of options.
1) Querying 1 by 1
I'm assuming you're going to be looping through every line of the CSV file anyway, and then making a new model? In which case, you can add an extra database call in here;
$model->manufacturer_id = Manufacturer::whereName($colXValue)->first()->id;
(You'd obviously need to put in your own checks etc. here to make sure manufacturers exist)
This method is fine relatively small datsets, however, if you're importing lots and lots of rows, it might end up sluggish with alot of arguably unnecessary database calls.
2) Mapping ALL your Manufacturers
Another option would be to create a local map of all your Manufacturers before you loop through your CSV lines;
$mappedManufacturers = Manufacturer::all()->pluck('id', 'name');
This will make $mappedManufacturers an array of manufacturers that has name as a key, id as a value. This way, when you're building your model, you can do;
$model->manufacturer_id = $mappedManufacturers[$colXValue];
This method is also fine, unless you have tens of thousands of Manufacturers!
3) Where in - then re-looping
Another option would be to build up a list of manufacturer names when looping through your CSV lines, going to the database with 1 whereIn query and then re-looping through your models to populate the manufacturer ID.
So in your initial loop through your CSV, you can temporarily set a property to store the name of the manufacturer, whilst adding it to another array;
$models = collect();
$model->..... = ....;
$model->manufacturer = $colXValue;
$models->push($colXValue);
Then you'll end up with a collection of models. You then query the database for ONLY manufacturers which have appeared:
$manufacturers = Manufacturer::whereIn('name', $models->lists('manufacturer'))->get()->keyBy('name')->toArray();
This will give you array of manufacturers, keyed by their name.
You then loop through your $models collection again, assigning the correct manufacturer id using the map;
$model->manufacturer_id = $manufacturers[$model->manufacturer];
Hopefully this will give you some ideas of how you can achieve this. I'd say the solution mostly depends on your use case - if this was going to be a heavy duty ask - I'd definitely Queue it and be tempted to use Option 1! :P

CakePHP 3 Fetch associated data recursive

I am trying to fetch Data in a recursive-way (over associations) using CakePHP 3.1
In Cake 3 I can use the "contain" key to fetch the next level of asociated data. But I need to fetch one more level. Does anyone know how to do this? I read the docs but didn't found anything there, with google it's the same.
The 3 Levels are connected like this:
OperationalCostInvoice (belongsTo Object)
-> Object (hasMany OperationalCostTypes)
-> OperationalCostType
With OperationalCostInvoice->get($object_id, ['contain' => 'Object']) I can get the Object that is associated with the OperationalCostInvoice but I also want to fetch the OperationalCostTypes from the Object in (if possible) just one call.
I dont need tipps about association linking the reason that the entities are linked like this is I can easily implement a history function.
Thanks in advance!
I just meant one function call (on the Table object) to fetch everything. I know that more than one query is required.
Just create your own table method then and return all your results in one array or implement whatever you want and return it.
public function foo() {
return [
'one' => $this->find()...->all();
'two' => $this->Asssoc->find()...->all();
];
}
But in CakePHP 2 there was the option recursive which controlled on how many levels associated data is fetched.
The recursive was a pretty stupid thing in Cake2. First thing we've always done was to set it to -1 in the AppModel to avoid unnecessary data fetching. Using contain is always the better choice. I would stay away from using recursive at all, especially for deeper levels.
Also contain is still, as it was in Cake2 as well, able to fetch more than one level deep associations.
$this->find()->contain([
'FirstLevel' => [
'SecondLevel' => [
'ThirdLevel'
]
]
])->all();

CakePhp Contain Model Cache

I'm using cakephp. In some query I use these kind of find:
$this->Photo->Behaviors->attach('Containable', array('autoFields' => true));
This is the Contain array that i use in the find:
'contain'=>array(
'User'=>array('fields'=>array('User.Name','User.Username')),
'Like' => array('User'=>array('fields'=>'Name'),
'order'=>'Timestamp DESC'
)),
'recursive' => 2,
The problem is that every time i want the Name of User that liked a photo. Cakephp does this query.
For example: SELECT `User`.`Name` FROM `Users` AS `User` WHERE `User`.`id` = 2175
If i have 300 likes on one photo i will make another 300queries for the User.Name. So, I would like to cache this kind of request. I've installed memcache correctly in my server, it's working normally. But I can't find a way to cache the query that cake make with the Containable Behaviors.
Has some one had this problem?
Thanks
G.
Why not use cakephp's counterCache feature instead?
There is a lot of documentation in the cakephp book about caching queries not just in me cache but many other ways as well such. Check out the CakeCache class.
there are a number of approaches on this topic.
if it is only about belongsTo relations it boils down to manually join tables using bindModel().
here is a good behavior: http://planetcakephp.org/aggregator/items/891-linkable-behavior-taking-it-easy-in-your-db
it takes care of that itself.

Choosing field names while finding results from two tables

I have two tables (which happen to be in two different databases). "clients" and "domains", clients can have multiple domains.
This is the code i am using:
$this->Domain->find('all', array(
'order' => 'domain ASC',
'fields' => array(
'Domain.id',
'Domain.domain',
'Server.name',
'Client.id',
'Client.name'
)
));
When i return all the fields by not using the 'fields' => array() everything works fine, as soon as i ask for specific fields, it says:
SQL Error: 1054: Unknown column
'Client.id' in 'field list'
Everything also works fine if i just remove the two Client columns (The Client model is the only model which is on another database.
If clients hasMany domains, So your models should be called like $this->Domain->find('all'); explicitly passing fields Client.id will show error, as its not the part of domains table, enable sql dumping using debug=2 and see how queries are being run.
You models should be
// in client.php model - having structure - id, name
$hasMany = 'Domain';
// in domain.php model - having structure - id, name, client_id
$belongsTo = 'Client';
This should work like this
$this->Domain->recursive = 1;
$data = $this->Domain->find('all');
// $data = Array ( 'Domain' => ********, 'Client' => ****** )
If your two tables are in different databases, you're really making your life difficult. AFAIK, Cake doesn't support joining two tables (or enforcing relationships) between two different databases. Why do you have the client table in a separate db?
If you can't move your table, I think you're going to have to write some custom code inside your Domain model, so that it will use the (default) db connection string for domains, but will instantiate and connect another db resource to the other database. See http://bakery.cakephp.org/articles/doze/2010/03/12/use-multiple-databases-in-one-app-based-on-requested-url for how to do it -- skip ahead to the section entitled, "Select Correct Database Dynamically".
HTH,
Travis

Querying data based on 3rd level relationship in CakePHP

I have the following relationships set up:
A HABTM B
B belongsTo C
C hasMany B
Now, for a given A, I need all C with the B's attached. I can write the SQL queries, but what's the proper CakePHP way? What method do I call on which model, and with which parameters?
I'd go with Aziz' answer and simply process the data as it comes in. If you need C to be your primary model though, you'll have to do a little workaround. Cake is not terrifically good with conditions on related models yet, especially on removed 3rd cousins kind of queries. It usually only does actual JOIN queries on belongsTo or hasMany relations; not on HABTM relations though, those it gets in separate queries. That means you can't include conditions on related HABTM models.
Your best bet then might be something like this:
// get related records as usual with the condition on A, limit to as little data as necessary
$ids = $this->A->find('first', array(
'conditions' => array('A.id' => 'something'),
'recursive' => 2,
'fields' => array('A.id'),
'contain' => array('B.id', 'B.c_id', 'B.C.id') // not quite sure if B.C.id works, maybe go with B.C instead
));
// find Cs, using the ids we got before as the condition
$Cs = $this->C->find('all', array(
'conditions' => array('C.id' => Set::extract('/B/C/id', $ids)),
'recursive => 1
);
Note that this produces quite a bunch of queries, so it's not really an optimal solution. Writing your own SQL might actually be the cleanest way.
EDIT:
Alternatively, you could re-bind your associations on the fly to make them hasMany/belongsTo relationships, most likely using the join table/model of A and B. That might enable you to use conditions on related models more easily, but it's still tricky to fetch Cs when the condition is on A.
$this->A->find(
'first',
array('conditions'=>array('id'=>'someword'),
'recursive'=>2)
);
like this?
this might work
$this->C->find('all',
array('conditions'=>array('id'=>'someword','B.aid'=>$a.id)));
I'd think of "Containable" behaviour (http://book.cakephp.org/view/474/Containable)... gives a lot control on finding related data.

Categories