There is more than one way to solve this problem, including what you describe.
I made the transition from relying on the GUI to relying on DSL instead. This means my projects are checked in as source code. While I sometimes start in the GUI, my finaly solution is always in DSL.
Once the solution is managed as code, the porting problem is partially solved - run the DSL script on the production server as needed once you verify it works on your dev server.
You are correct to note objects are not deleted, but instead overwritten. The undesired side-effects are that if your change your model and remove something in a new definition, the object will persist. If instead you are replacing objects, they will be re-defined. As I develop my DSL model, I find I start with coarse delete operations (project), and then substitute finer deletes (component definition) as necessary. This means that in a steady-state operation, I'm mostly replacing and not deleting objects. The shift from coarse- to fine-grained delete is an iterative process for me.
I have wrapped my DSL with some cleanup steps. While it may not be practical to delete the project, maybe it is practical to delete items by name:
ectool deleteProject "myProject" - may be too ambitions
ectool deleteApplication --projectName "Example" --applicationName "Single Application 1 Comp"
ectool deletePipeline --projectName "myProjectName" --pipelineName "the name of your pipeline"
Deleting an Environment deletes mappings into it, so that may help part of your problem about the mappings.