Using ObjectContext. I'm wanting to do this by passing an SQL query via the ExecuteStoreCommand since I don't fancy retrieving all relevant entities just for the sake of deleting them after.
The Category table is as so:
CatID | CatName | ParentID
Where CatID is the primary key to the ParentID FK
I am wishing to delete a category and also all those that
are under it. Can be 2+ levels deep of sub cats, so different ParentID's
Thought I could do it as below and just set "cascade" on delete option
for the foreign key in the database, but it won't let me and it does not appear to want to
cascade delete down by using the CatID - ParentID relationship and the query gets
stopped by this very FK constraint.
public RedirectToRouteResult DelCat(int CatID)
{
if (CatID != 0)
{
_db.ExecuteStoreCommand("DELETE FROM Categories WHERE CatID={0}", CatID);
_db.SaveChanges();
}
return RedirectToAction("CatManage");
}
Recursive CTE allCategories produces list of all categories in hierarchy. Delete part, obviously, deletes them all.
; with allCategories as (
select CatID
from Categories
where CatID = #CatID_to_delete
union all
select Categories.CatID
from allCategories
inner join Categories
on allCategories.CatID = Categories.ParentID
)
delete Categories
from Categories
inner join allCategories
on Categories.CatID = allCategories.CatID
Try it with select * from allCategories, though, to check first.
There is TEST # Sql Fiddle.
Why not just send two statements in your batch?
DELETE Categories WHERE ParentID = {0};
DELETE Categories WHERE CatID = {0};
If the framework you're using "won't let you" do that, then do this right: put your logic in a stored procedure, and call the stored procedure.
Related
Question
When dealing with a one-to-many or many-to-many SQL relationship in Golang, what is the best (efficient, recommended, "Go-like") way of mapping the rows to a struct?
Taking the example setup below I have tried to detail some approaches with Pros and Cons of each but was wondering what the community recommends.
Requirements
Works with PostgreSQL (can be generic but not include MySQL/Oracle specific features)
Efficiency - No brute forcing every combination
No ORM - Ideally using only database/sql and jmoiron/sqlx
Example
For sake of clarity I have removed error handling
Models
type Tag struct {
ID int
Name string
}
type Item struct {
ID int
Tags []Tag
}
Database
CREATE TABLE item (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
);
CREATE TABLE tag (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(160),
item_id INT REFERENCES item(id)
);
Approach 1 - Select all Items, then select tags per item
var items []Item
sqlxdb.Select(&items, "SELECT * FROM item")
for i, item := range items {
var tags []Tag
sqlxdb.Select(&tags, "SELECT * FROM tag WHERE item_id = $1", item.ID)
items[i].Tags = tags
}
Pros
Simple
Easy to understand
Cons
Inefficient with the number of database queries increasing proportional with number of items
Approach 2 - Construct SQL join and loop through rows manually
var itemTags = make(map[int][]Tag)
var items = []Item{}
rows, _ := sqlxdb.Queryx("SELECT i.id, t.id, t.name FROM item AS i JOIN tag AS t ON t.item_id = i.id")
for rows.Next() {
var (
itemID int
tagID int
tagName string
)
rows.Scan(&itemID, &tagID, &tagName)
if tags, ok := itemTags[itemID]; ok {
itemTags[itemID] = append(tags, Tag{ID: tagID, Name: tagName,})
} else {
itemTags[itemID] = []Tag{Tag{ID: tagID, Name: tagName,}}
}
}
for itemID, tags := range itemTags {
items = append(Item{
ID: itemID,
Tags: tags,
})
}
Pros
A single database call and cursor that can be looped through without eating too much memory
Cons
Complicated and harder to develop with multiple joins and many attributes on the struct
Not too performant; more memory usage and processing time vs. more network calls
Failed approach 3 - sqlx struct scanning
Despite failing I want to include this approach as I find it to be my current aim of efficiency paired with development simplicity. My hope was by explicitly setting the db tag on each struct field sqlx could do some advanced struct scanning
var items []Item
sqlxdb.Select(&items, "SELECT i.id AS item_id, t.id AS tag_id, t.name AS tag_name FROM item AS i JOIN tag AS t ON t.item_id = i.id")
Unfortunately this errors out as missing destination name tag_id in *[]Item leading me to believe the StructScan is not advanced enough to recursively loop through rows (no criticism - it is a complicated scenario)
Possible approach 4 - PostgreSQL array aggregators and GROUP BY
While I am sure this will not work I have included this untested option to see if it could be improved upon so it may work.
var items = []Item{}
sqlxdb.Select(&items, "SELECT i.id as item_id, array_agg(t.*) as tags FROM item AS i JOIN tag AS t ON t.item_id = i.id GROUP BY i.id")
When I have some time I will try and run some experiments here.
the sql in postgres :
create schema temp;
set search_path = temp;
create table item
(
id INT generated by default as identity primary key
);
create table tag
(
id INT generated by default as identity primary key,
name VARCHAR(160),
item_id INT references item (id)
);
create view item_tags as
select id,
(
select
array_to_json(array_agg(row_to_json(taglist.*))) as array_to_json
from (
select tag.name, tag.id
from tag
where item_id = item.id
) taglist ) as tags
from item ;
-- golang query this maybe
select row_to_json(row)
from (
select * from item_tags
) row;
then golang query this sql:
select row_to_json(row)
from (
select * from item_tags
) row;
and unmarshall to go struct:
pro:
postgres manage the relation of data. add / update data with sql functions.
golang manage business model and logic.
it's easy way.
.
I can suggest another approach which I have used before.
You make a json of the tags in this case in the query and return it.
Pros: You have 1 call to the db, which aggregates the data, and all you have to do is parse the json into an array.
Cons: It's a bit ugly. Feel free to bash me for it.
type jointItem struct {
Item
ParsedTags string
Tags []Tag `gorm:"-"`
}
var jointItems []*jointItem
db.Raw(`SELECT
items.*,
(SELECT CONCAT(
'[',
GROUP_CONCAT(
JSON_OBJECT('id', id,
'name', name
)
),
']'
)) as parsed_tags
FROM items`).Scan(&jointItems)
for _, o := range jointItems {
var tempTags []Tag
if err := json.Unmarshall(o.ParsedTags, &tempTags) ; err != nil {
// do something
}
o.Tags = tempTags
}
Edit: code might behave weirdly so I find it better to use a temporary tags array when moving instead of using the same struct.
You can use carta.Map() from https://github.com/jackskj/carta
It tracks has-many relationships automatically.
I have a table from which I create a tree with multiple levels and parents. The table structure looks like this.
When I delete the "TitleID", I want all the children and even the grandchildren to be deleted.
What is the easiest way to do such in sql.
If I simple delete with "where ParentID=TitleID", only children with level 1 depth are deleted.
DECLARE #TitleId INT
SELECT ##TitleId = 2
;WITH results AS(
SELECT TitleId
FROM myTable
WHERE TitleId = #TitleId
UNION ALL
SELECT t.TitleId
FROM myTable t
INNER JOIN ret r ON t.ParentID = r.TitleId
)
DELETE FROM myTable WHERE TitleId IN (SELECT TitleId FROM results )
To handle tree structured data in relational database, you can add another column FullID, which contains value like 1.1.3. Then what you need is just a simple where clause WHERE FullID LIKE '1.1.%' if you want to delete node 1.1 and it's children.
The value of FullID can be generated by a stored procedure (for old data), or better by your application (for new data).
I am using SQL Server 2008 R2. I have imported 2 tables from excel and I want to link them together. I looks like this:
Tables imported from Excel
brand (nvarchar(20) name)
models (nvarchar(20) parent, nvarchar(50 name))
Tables after my amends
brand (int ident id, nvarchar(20) name, tinyint status)
models (int ident id, int parent_id,
nvarchar(20) parent, nvarchar(50) name, tinyint status)
As you can see I'd like to link table models using parent_id to table brand using id.
Select is ok, I have done that.
What I need is create bulk update which would put brand id into model parent_id.
Conditions are:
set models.parent_id = brand.id where brand.name = model.parent
I hope it is clear. Basically I want to change linking field model.parent to model.parent_id. There is a possibility that brand.name can change and if that happens table models would be unable to link to correct parent.
And I want to do that in bulk, to go through all the records in brand and update all relevant records in models.
UPDATE
m
SET
parent_id = b.id
FROM
models m
JOIN
brand b ON b.name = m.parent
I'd them assume you want to remove models.parent
ALTER TABLE models DROP COLUMN parent
UPDATE models
SET parent_id = brand.id
FROM brand
WHERE brand.name = models.parent
I have two tables A & B, and B has a many:1 relationship with A.
When querying rows from A I'd also like to have corresponding B records returned as an array and added to the result array from A, so I end up with something like this:
A-ROW
field
field
B-ITEMS
item1
item2
item3
Is there a clean way to do this with one query (perhaps a join?), or should I just perform a second query of B on the id from A and add that to the result array?
It would be more efficient to join table B on table A. It will not give you the data in the shape you are looking for. But you can iterate over this result and build the data into the desired shape.
Here is some code to illustrate the idea :
// Join table B on table A through a foreign key
$sql = 'select a.id, a.x, b.y
from a
left join b on b.a_id=a.id
order by a.id';
// Execute query
$result = $this->db->query($sql)->result_array();
// Initialise desired result
$shaped_result = array();
// Loop through the SQL result creating the data in your desired shape
foreach ($result as $row)
{
// The primary key of A
$id = $row['id'];
// Add a new result row for A if we have not come across this key before
if (!array_key_exists($id, $shaped_result))
{
$shaped_result[$id] = array('id' => $id, 'x' => $row['x'], 'b_items' => array());
}
if ($row['y'] != null)
{
// Push B item onto sub array
$shaped_result[$id]['b_items'][] = $row['y'];
}
}
"... just perform a second query of B on the id from A and add that to the result array ..." -- that is the correct solution. SQL won't comprehend nested array structure.
To build on what Smandoli said--
Running the secondary query separately is more efficient because even if row data on the primary table (A) has changed, unchanged data on the secondary table (B) will result in a (MySQL) query cache hit assuming the IDs never change.
This is not necessarily true of the join query approach.
There will also be less data coming over the wire since the join approach will fetch duplicate data for the primary table (A) if the secondary table (B) has multiple rows associated with a single row in the primary table.
Hopefully anyone looking to do this (relatively) common type of data retrieval may find this useful.
Below is my (simplified) schema (in MySQL ver. 5.0.51b) and my strategy for updating it. There has got to be a better way. Inserting a new item requires 4 trips to the database and editing/updating an item takes up to 7!
items: itemId, itemName
categories: catId, catName
map: mapId*, itemId, catId
* mapId (varchar) is concat of itemId + | + catId
1) If inserting: insert item. Get itemId via MySQL API.
Else updating: just update the item table. We already have the itemId.
2) Conditionally batch insert into categories.
INSERT IGNORE INTO categories (catName)
VALUES ('each'), ('category'), ('name');
3) Select IDs from categories.
SELECT catId FROM categories
WHERE catName = 'each' OR catName = 'category' OR catName = 'name';
4) Conditionally batch insert into map.
INSERT IGNORE INTO map (mapId, itemId, catId)
VALUES ('1|1', 1, 1), ('1|2', 1, 2), ('1|3', 1, 3);
If inserting: we're done. Else updating: continue.
5) It's possible that we no longer associate a category with this item that we did prior to the update. Delete old categories for this itemId.
DELETE FROM MAP WHERE itemId = 2
AND catID <> 2 AND catID <> 3 AND catID <> 5;
6) If we have disassociated ourselves from a category, it's possible that we left it orphaned. We do not want categories with no items. Therefore, if affected rows > 0, kill orphaned categories. I haven't found a way to combine these in MySQL, so this is #6 & #7.
SELECT categories.catId
FROM categories
LEFT JOIN map USING (catId)
GROUP BY categories.catId
HAVING COUNT(map.catId) < 1;
7) Delete IDs found in step 6.
DELETE FROM categories
WHERE catId = 9
AND catId = 10;
Please tell me there's a better way that I'm not seeing.
Also, if you are worried about trips to the db, make steps into a stored procedure. Then you have one trip.
There are a number of things you can do to make a bit easier:
Read about [INSERT...ON DUPLICATE KEY UPDATE][1]
Delete old categories before you insert new categories. This may benefit from an index better.
DELETE FROM map WHERE itemId=2;
You probably don't need map.mapID. Instead, declare a compound primary key over (itemID, catID).
As Peter says in his answer, use MySQL's multi-table delete:
DELETE categories.* FROM categories LEFT JOIN map USING (catId)
WHERE map.catID IS NULL
http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html
Steps 6 & 7 can be combined easily enough:
DELETE categories.*
FROM categories
LEFT JOIN map USING (catId)
WHERE map.catID IS NULL;
Steps 3 & 4 can also be combined:
INSERT IGNORE INTO map (mapId, itemId, catId)
SELECT CONCAT('1|', c.catId), 1, c.catID
FROM categories AS c
WHERE c.catName IN('each','category','name');
Otherwise, your solution is pretty standard, unless you want to use triggers to maintain the map table.