How about using
notebookHasData[] := Length[Cells[CellTags -> "data"]] > 0; myData[] := RandomReal[1, 1000];
and
If[ notebookHasData[], NotebookEvaluate[EvaluationNotebook[], EvaluationElements -> {"Tags" -> {"data"}}, InsertResults -> True]; , data = myData[]; NotebookDelete[Cells[CellTags -> "data"]]; CellPrint@ Cell[BoxData@ ReleaseHold@ Hold[MakeBoxes][Hold[Set][Hold[data], Hold@Evaluate@data]], "Input", CellTags -> "data"] ]
evaluating the big piece of code generates a cell that looks like

It checks if such a cell already exists. If it does, it evaluates that cell, rather than generating the data and the cell again.
This still requires you to evaluate one cell, whether it is the generated cell or a cell containing/refering to the "big piece of code" I show above. You could further automate this process by using an InitializationCell (which will prompt you), or by using some more advanced stuff using Dynamics or CellEvaluationFunction (this is hacky), which allow for solutions that will not prompt you.
This stores the data in quite an ineffecient way, with a lot of RowBoxes. We could make a text cell to store all the numbers and maybe make it invisible. Then we could probably make a function to extract the data from this cell by some other means than evaluation.
Do you want to share your notebooks with others and is that the reason you don't want to work with files? If there is no special reason, my advice would be not to get too perfectionistic about this, as I think solutions using files are usually better.
SaveDefinitions->TruewithManipulateor atDynamicModuleas these should address your problem. $\endgroup$