Skip to content

Commit e139e57

Browse files
authored
Merge pull request #20 from itosho/add-select-insert-method
Support "INSERT SELECT" query for inserting a record just once
2 parents 4cceca7 + ae06998 commit e139e57

File tree

9 files changed

+381
-22
lines changed

9 files changed

+381
-22
lines changed

.semver

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
:major: 1
3-
:minor: 1
4-
:patch: 1
3+
:minor: 2
4+
:patch: 0
55
:special: ''
66
:metadata: ''

.travis.yml

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
language: php
22

3+
services:
4+
- mysql
5+
36
php:
47
- 7.0
58
- 7.1
69
- 7.2
10+
- 7.3
711

812
sudo: false
913

1014
env:
1115
matrix:
12-
- DB=mysql db_dsn='mysql://root@0.0.0.0/cakephp_test'
16+
- DB=mysql db_dsn='mysql://travis@127.0.0.1/cakephp_test'
1317

1418
global:
1519
- DEFAULT=1
@@ -18,32 +22,42 @@ matrix:
1822
fast_finish: true
1923

2024
include:
21-
- php: 7.1
25+
- php: 7.3
2226
env: PHPCS=1 DEFAULT=0
2327

24-
- php: 7.1
28+
- php: 7.3
2529
env: PHPSTAN=1 DEFAULT=0
2630

31+
cache:
32+
directories:
33+
- vendor
34+
- $HOME/.composer/cache
35+
2736
before_script:
28-
- if [[ $TRAVIS_PHP_VERSION != 7.1 ]]; then phpenv config-rm xdebug.ini; fi
37+
- if [[ $TRAVIS_PHP_VERSION != 7.3 ]]; then phpenv config-rm xdebug.ini; fi
2938

3039
- composer self-update
3140
- composer install --prefer-dist --no-interaction
3241

33-
- if [[ $DB = 'mysql' ]]; then mysql -e 'CREATE DATABASE cakephp_test;'; fi
42+
- if [[ $DB = 'mysql' ]]; then mysql -e 'CREATE DATABASE cakephp_test; GRANT ALL PRIVILEGES ON cakephp_test.* TO travis@localhost;'; fi
3443

3544
- if [[ $PHPCS = 1 ]]; then composer require cakephp/cakephp-codesniffer:"^3.0"; fi
3645
- if [[ $PHPSTAN = 1 ]]; then composer require phpstan/phpstan; fi
3746

3847
script:
39-
- if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION != 7.1 ]]; then vendor/bin/phpunit; fi
40-
- if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.1 ]]; then vendor/bin/phpunit --coverage-clover=clover.xml; fi
48+
- if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION != 7.3 ]]; then vendor/bin/phpunit; fi
49+
- if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.3 ]]; then vendor/bin/phpunit --coverage-clover=clover.xml; fi
4150

4251
- if [[ $PHPCS = 1 ]]; then vendor/bin/phpcs -n -p --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests; fi
4352
- if [[ $PHPSTAN = 1 ]]; then vendor/bin/phpstan analyse -c phpstan.neon -l 7 src; fi
4453

4554
after_success:
46-
- if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.1 ]]; then bash <(curl -s https://codecov.io/bash); fi
55+
- |
56+
if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.3 ]]; then
57+
curl -s https://codecov.io/bash > codecov
58+
sed -i -e 's/TRAVIS_.*_VERSION/^TRAVIS_.*_VERSION=/' codecov
59+
bash codecov
60+
fi
4761
4862
notifications:
4963
email: false

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGES
22

3+
## v1.2.0
4+
- :sparkles: Add insert select query for inserting a record just once.
5+
- :green_heart: Improve CI.
6+
37
## v1.1.1
48
- :up: Correspond to CakePHP 3.5 or higher version.
59
- :green_heart: Improve CI.

README.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Easy Query
22

3-
CakePHP behavior plugin for easily some complicated queries like upsert, bulk upsert and bulk insert.
3+
CakePHP behavior plugin for easily some complicated queries.
44

55
[![Build Status](https://travis-ci.org/itosho/easy-query.svg?branch=master)](https://travis-ci.org/itosho/easy-query)
66
[![codecov](https://codecov.io/gh/itosho/easy-query/branch/master/graph/badge.svg)](https://codecov.io/gh/itosho/easy-query)
@@ -83,6 +83,62 @@ $entities = $this->Articles->newEntities($data);
8383
$this->Articles->bulkInsert($entities);
8484
```
8585

86+
### Insert Select
87+
For inserting a record just once.
88+
89+
#### case1
90+
Specify search conditions.
91+
92+
```php
93+
$this->Articles = TableRegistry::get('Articles');
94+
$this->Articles->addBehavior('Itosho/EasyQuery.Insert');
95+
96+
$data = [
97+
'title' => 'New Article?',
98+
'body' => 'New Article Body?'
99+
];
100+
$entity = $this->Articles->newEntity($data);
101+
$condition = [
102+
'title' => 'New Article?'
103+
];
104+
105+
$this->Articles->insertOnce($entities);
106+
```
107+
108+
Generated SQL is below.
109+
110+
```sql
111+
INSERT INTO articles (title, body)
112+
SELECT 'New Article?', 'New Article Body?' FROM tmp WHERE NOT EXISTS (
113+
SELECT * FROM articles WHERE title = 'New Article?'
114+
)
115+
```
116+
117+
#### case2
118+
Auto set search conditions with a inserting record.
119+
120+
```php
121+
$this->Articles = TableRegistry::get('Articles');
122+
$this->Articles->addBehavior('Itosho/EasyQuery.Insert');
123+
124+
$data = [
125+
'title' => 'New Article',
126+
'body' => 'New Article Body'
127+
];
128+
$entity = $this->Articles->newEntity($data);
129+
130+
$this->Articles->insertOnce($entities);
131+
```
132+
133+
Generated SQL is below.
134+
135+
```sql
136+
INSERT INTO articles (title, body)
137+
SELECT 'New Article', 'New Article Body' FROM tmp WHERE NOT EXISTS (
138+
SELECT * FROM articles WHERE title = 'New Article' AND body = 'New Article Body'
139+
)
140+
```
141+
86142
### Advanced
87143
Need to use `Timestamp` behavior, if you want to update `created` and `modified` fields automatically.
88144
And you can change the action manually by using `event` config like this.

composer.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
{
22
"name": "itosho/easy-query",
3-
"description": "CakePHP behavior plugin for easily some complicated queries like upsert, bulk upsert and bulk insert.",
3+
"description": "CakePHP behavior plugin for easily some complicated queries.",
44
"type": "cakephp-plugin",
55
"keywords": [
66
"cakephp",
77
"behavior",
88
"plugin",
99
"upsert",
10-
"bulk",
11-
"insert"
10+
"bulk upsert",
11+
"bulk insert",
12+
"insert select",
13+
"insert once"
1214
],
1315
"homepage": "https://github.com/itosho/easy-query",
1416
"license": "MIT",

src/Model/Behavior/InsertBehavior.php

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
namespace Itosho\EasyQuery\Model\Behavior;
44

5+
use Cake\Database\Expression\QueryExpression;
6+
use Cake\Database\StatementInterface;
7+
use Cake\I18n\FrozenTime;
58
use Cake\ORM\Behavior;
9+
use Cake\ORM\Entity;
10+
use Cake\ORM\Query;
11+
use Cake\ORM\TableRegistry;
612
use LogicException;
713

814
/**
@@ -22,9 +28,9 @@ class InsertBehavior extends Behavior
2228
/**
2329
* execute bulk insert query
2430
*
25-
* @param \Cake\ORM\Entity[] $entities insert entities
31+
* @param Entity[] $entities insert entities
2632
* @throws LogicException no save data
27-
* @return \Cake\Database\StatementInterface query result
33+
* @return StatementInterface query result
2834
*/
2935
public function bulkInsert(array $entities)
3036
{
@@ -49,4 +55,113 @@ public function bulkInsert(array $entities)
4955

5056
return $query->execute();
5157
}
58+
59+
/**
60+
* execute insert select query for saving a record just once
61+
*
62+
* @param Entity $entity insert entity
63+
* @param array|null $conditions search conditions
64+
* @return StatementInterface query result
65+
*/
66+
public function insertOnce(Entity $entity, array $conditions = null)
67+
{
68+
if ($this->_config['event']['beforeSave']) {
69+
$this->_table->dispatchEvent('Model.beforeSave', compact('entity'));
70+
}
71+
72+
$entity->setVirtual([]);
73+
$insertData = $entity->toArray();
74+
if (isset($insertData['created']) && !is_null($insertData['created'])) {
75+
$insertData['created'] = FrozenTime::now()->toDateTimeString();
76+
}
77+
if (isset($insertData['modified']) && !is_null($insertData['modified'])) {
78+
$insertData['modified'] = FrozenTime::now()->toDateTimeString();
79+
}
80+
81+
$escape = function ($content) {
82+
return is_null($content) ? 'NULL' : '\'' . addslashes($content) . '\'';
83+
};
84+
85+
$escapedInsertData = array_map($escape, $insertData);
86+
$fields = array_keys($insertData);
87+
$existsConditions = $conditions;
88+
if (is_null($existsConditions)) {
89+
$existsConditions = $this->getExistsConditions($escapedInsertData);
90+
}
91+
92+
$query = $this->_table
93+
->query()
94+
->insert($fields)
95+
->epilog(
96+
$this
97+
->buildTmpTableSelectQuery($escapedInsertData)
98+
->where(function (QueryExpression $exp) use ($existsConditions) {
99+
$query = $this->_table
100+
->find()
101+
->where($existsConditions);
102+
103+
return $exp->notExists($query);
104+
})
105+
->limit(1)
106+
);
107+
108+
return $query->execute();
109+
}
110+
111+
/**
112+
* build tmp table's select query for insert select query
113+
*
114+
* @param array $escapedData escaped array data
115+
* @throws LogicException select query is invalid
116+
* @return Query tmp table's select query
117+
*/
118+
private function buildTmpTableSelectQuery($escapedData)
119+
{
120+
$driver = $this->_table
121+
->getConnection()
122+
->getDriver();
123+
$schema = [];
124+
foreach ($escapedData as $key => $value) {
125+
$col = $driver->quoteIdentifier($key);
126+
$schema[] = "{$value} AS {$col}";
127+
}
128+
129+
$tmpTable = TableRegistry::getTableLocator()->get('tmp', [
130+
'schema' => $this->_table->getSchema()
131+
]);
132+
$query = $tmpTable
133+
->find()
134+
->select(array_keys($escapedData))
135+
->from(
136+
sprintf('(SELECT %s) as tmp', implode(',', $schema))
137+
);
138+
/** @var Query $selectQuery */
139+
$selectQuery = $query;
140+
141+
return $selectQuery;
142+
}
143+
144+
/**
145+
* get conditions for finding a record already exists
146+
*
147+
* @param array $escapedData escaped array data
148+
* @return array conditions
149+
*/
150+
private function getExistsConditions($escapedData)
151+
{
152+
$autoFillFields = ['created', 'modified'];
153+
$existsConditions = [];
154+
foreach ($escapedData as $field => $value) {
155+
if (in_array($field, $autoFillFields, true)) {
156+
continue;
157+
}
158+
if ($value === 'NULL') {
159+
$existsConditions[] = "{$field} IS NULL";
160+
} else {
161+
$existsConditions[] = "{$field} = {$value}";
162+
}
163+
}
164+
165+
return $existsConditions;
166+
}
52167
}

src/Model/Behavior/UpsertBehavior.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Itosho\EasyQuery\Model\Behavior;
44

5+
use Cake\Database\StatementInterface;
6+
use Cake\Datasource\EntityInterface;
57
use Cake\ORM\Behavior;
68
use Cake\ORM\Entity;
79
use LogicException;
@@ -25,9 +27,9 @@ class UpsertBehavior extends Behavior
2527
/**
2628
* execute upsert query
2729
*
28-
* @param \Cake\ORM\Entity $entity upsert entity
30+
* @param Entity $entity upsert entity
31+
* @return EntityInterface|array|null result entity
2932
* @throws LogicException invalid config
30-
* @return \Cake\Datasource\EntityInterface|array|null result entity
3133
*/
3234
public function upsert(Entity $entity)
3335
{
@@ -79,9 +81,9 @@ public function upsert(Entity $entity)
7981
/**
8082
* execute bulk upsert query
8183
*
82-
* @param \Cake\ORM\Entity[] $entities upsert entities
84+
* @param Entity[] $entities upsert entities
85+
* @return StatementInterface query result
8386
* @throws LogicException invalid config or no save data
84-
* @return \Cake\Database\StatementInterface query result
8587
*/
8688
public function bulkUpsert(array $entities)
8789
{

0 commit comments

Comments
 (0)